diff --git a/app/globals.css b/app/globals.css
index 9cb7d82..fe95fca 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -88,6 +88,30 @@
.vn-scrollbar::-webkit-scrollbar-corner {
background: transparent;
}
+
+ /* 极细滚动条 · 无轨道背景 */
+ .thin-scrollbar {
+ scrollbar-width: thin;
+ scrollbar-color: rgba(195, 155, 75, 0.5) transparent;
+ }
+
+ .thin-scrollbar::-webkit-scrollbar {
+ width: 4px;
+ height: 4px;
+ }
+
+ .thin-scrollbar::-webkit-scrollbar-track {
+ background: transparent;
+ }
+
+ .thin-scrollbar::-webkit-scrollbar-thumb {
+ background: rgba(195, 155, 75, 0.45);
+ border-radius: 999px;
+ }
+
+ .thin-scrollbar::-webkit-scrollbar-thumb:hover {
+ background: rgba(220, 180, 95, 0.7);
+ }
}
@keyframes infiplot-ripple {
diff --git a/app/page.tsx b/app/page.tsx
index b50cf1e..6d61a19 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -12,6 +12,9 @@ import {
} from "@/lib/options";
import { readStoredTtsConfig } from "@/lib/clientTtsConfig";
import { SettingsModal, readStoredPlayerName, readStoredVisionClick } from "@/components/SettingsModal";
+import { analyzeImageDataUrl } from "@infiplot/ai-client";
+import { readStoredModelConfig, resolveEngineConfig } from "@/lib/clientModelConfig";
+import { STYLE_EXTRACTION_PROMPT } from "@/lib/styleExtraction";
import { STORY_SHARE_STORAGE_KEY, parseStoryShareDoc } from "@/lib/storyShare";
/* ============================================================================
@@ -976,17 +979,33 @@ function StyleModal({
setParsing(true);
try {
const resized = await resizeImageToDataUrl(file);
- const res = await fetch("/api/parse-style-image", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ imageDataUrl: resized }),
- });
- if (!res.ok) {
- const j = (await res.json().catch(() => ({}))) as { error?: string };
- throw new Error(j.error ?? `${res.status}`);
+ const modelCfg = readStoredModelConfig();
+ let stylePrompt: string;
+ if (modelCfg) {
+ const config = resolveEngineConfig(modelCfg, null);
+ const raw = await analyzeImageDataUrl(config.vision, resized, STYLE_EXTRACTION_PROMPT);
+ let parsed: { stylePrompt?: string };
+ try {
+ parsed = JSON.parse(raw);
+ } catch {
+ parsed = { stylePrompt: raw };
+ }
+ stylePrompt = (parsed.stylePrompt ?? "").trim();
+ } else {
+ const r = await fetch("/api/parse-style-image", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ imageDataUrl: resized }),
+ });
+ if (!r.ok) {
+ const data = await r.json().catch(() => ({}));
+ throw new Error(data.error || `HTTP ${r.status}`);
+ }
+ const data = (await r.json()) as { stylePrompt?: string };
+ stylePrompt = (data.stylePrompt ?? "").trim();
}
- const data = (await res.json()) as { stylePrompt: string };
- setDraft(data.stylePrompt);
+ if (!stylePrompt) throw new Error("视觉模型返回了空的风格描述");
+ setDraft(stylePrompt);
setCustomStyleRefImage(resized);
track("style_image_upload", { ok: true });
} catch (err) {
@@ -1256,8 +1275,9 @@ export default function HomePage() {
// 顶部使用提示:默认展示,用户可点 × 永久关闭(localStorage:infiplot:hintClosed)。
const [hintClosed, setHintClosed] = useState(false);
- // 统一设置弹窗(名字 + 识图 + TTS Key):可选增强,数据只存浏览器。
+ // 统一设置弹窗(通用 + 模型):可选增强,数据只存浏览器。
const [settingsOpen, setSettingsOpen] = useState(false);
+ const [settingsTab, setSettingsTab] = useState<"general" | "models">("general");
const [ttsConfigured, setTtsConfigured] = useState(false);
const [playerName, setPlayerName] = useState("");
const [visionClickEnabled, setVisionClickEnabled] = useState(true);
@@ -1477,7 +1497,10 @@ export default function HomePage() {