+
{themeOptions.map((option) => (
setTheme(value)}
/>
))}
@@ -144,19 +136,16 @@ function ThemePreviewCard({
description,
active,
mode,
- systemTheme,
onSelect,
}: {
icon: ComponentType>;
label: string;
description: string;
active: boolean;
- mode: "system" | "light" | "dark";
- systemTheme?: string;
- onSelect: (mode: "system" | "light" | "dark") => void;
+ mode: "light" | "dark";
+ onSelect: (mode: "light" | "dark") => void;
}) {
- const previewMode =
- mode === "system" ? (systemTheme === "dark" ? "dark" : "light") : mode;
+ const previewMode = mode;
return (
{isSidebarOpen && }
+ {isSidebarOpen && }
{!compactChrome && (
diff --git a/frontend-web/src/contexts/ThemeContext.tsx b/frontend-web/src/contexts/ThemeContext.tsx
index a4042ae7..55cda911 100644
--- a/frontend-web/src/contexts/ThemeContext.tsx
+++ b/frontend-web/src/contexts/ThemeContext.tsx
@@ -1,4 +1,11 @@
-import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
+import React, {
+ createContext,
+ useContext,
+ useEffect,
+ useState,
+ type ReactNode,
+} from 'react';
+import { useTheme as useNextTheme } from 'next-themes';
// 主题类型
export type ThemeType = 'dark' | 'light';
@@ -75,12 +82,24 @@ const themes: Record = {
},
};
+const STORAGE_KEY = 'strategy-theme';
+
+function readPersistedTheme(): ThemeType {
+ if (typeof window === 'undefined') {
+ return 'dark';
+ }
+ const saved = localStorage.getItem(STORAGE_KEY);
+ return saved === 'light' ? 'light' : 'dark';
+}
+
// 主题上下文
interface ThemeContextType {
theme: ThemeType;
themeConfig: ThemeConfig;
toggleTheme: () => void;
setTheme: (theme: ThemeType) => void;
+ /** next-themes 完成挂载前为 false,避免 hydration 与 DOM 不同步 */
+ mounted: boolean;
}
const ThemeContext = createContext(undefined);
@@ -91,34 +110,52 @@ interface ThemeProviderProps {
}
export const ThemeProvider: React.FC = ({ children }) => {
- const [theme, setThemeState] = useState(() => {
- // 从 localStorage 读取保存的主题
- const saved = localStorage.getItem('strategy-theme');
- return (saved as ThemeType) || 'dark';
- });
+ const { resolvedTheme, setTheme: setNextTheme } = useNextTheme();
+ const [mounted, setMounted] = useState(false);
- // 切换主题
- const toggleTheme = () => {
- setThemeState((prev) => (prev === 'dark' ? 'light' : 'dark'));
- };
-
- // 设置特定主题
- const setTheme = (newTheme: ThemeType) => {
- setThemeState(newTheme);
- };
-
- // 保存到 localStorage
useEffect(() => {
- localStorage.setItem('strategy-theme', theme);
- // 更新文档类名
- document.documentElement.classList.remove('dark', 'light');
- document.documentElement.classList.add(theme);
- }, [theme]);
+ setMounted(true);
+ }, []);
+
+ // 迁移 / 规范化 localStorage,避免与 next-themes 冲突导致闪回浅色
+ useEffect(() => {
+ if (!mounted) return;
+ const current = localStorage.getItem(STORAGE_KEY);
+ const legacy = localStorage.getItem('theme');
+
+ if (current !== 'light' && current !== 'dark') {
+ const fallback =
+ legacy === 'light' || legacy === 'dark' ? legacy : 'dark';
+ setNextTheme(fallback);
+ } else if (!current && legacy && (legacy === 'light' || legacy === 'dark')) {
+ setNextTheme(legacy);
+ }
+
+ if (legacy) {
+ localStorage.removeItem('theme');
+ }
+ }, [mounted, setNextTheme]);
+
+ const theme: ThemeType = mounted
+ ? resolvedTheme === 'light'
+ ? 'light'
+ : 'dark'
+ : readPersistedTheme();
+
+ const toggleTheme = () => {
+ setNextTheme(theme === 'dark' ? 'light' : 'dark');
+ };
+
+ const setTheme = (newTheme: ThemeType) => {
+ setNextTheme(newTheme);
+ };
const themeConfig = themes[theme];
return (
-
+
{children}
);
diff --git a/frontend-web/src/core/messages/search-results.ts b/frontend-web/src/core/messages/search-results.ts
index 5da47164..7871d423 100644
--- a/frontend-web/src/core/messages/search-results.ts
+++ b/frontend-web/src/core/messages/search-results.ts
@@ -277,9 +277,14 @@ function normalizeSearchResultItem(raw: unknown): SearchResultItem | null {
const layers = collectNestedRecords(record);
+ const indexLabel = pickString(record.index, record.idx, record.rank, record.no);
+
let title =
pickFromLayers(layers, TITLE_ALIASES) ??
pickByKeyPattern(layers, TITLE_KEY_PATTERN, { maxLength: 200 });
+ if (!title && indexLabel) {
+ title = `结果 ${indexLabel}`;
+ }
let content =
pickFromLayers(layers, CONTENT_ALIASES) ??
pickByKeyPattern(layers, CONTENT_KEY_PATTERN, { excludeUrl: true });
@@ -313,7 +318,8 @@ function normalizeSearchResultItem(raw: unknown): SearchResultItem | null {
title = pickShortTitleFallback(layers, reserved);
}
- if (!title && !content && !url) {
+ // 内网检索可能只有 index + 空 title/content_preview,仍展示占位卡片
+ if (!title && !content && !url && !indexLabel) {
return null;
}
diff --git a/frontend-web/src/core/messages/utils.ts b/frontend-web/src/core/messages/utils.ts
index 6929f0a7..f13c5747 100644
--- a/frontend-web/src/core/messages/utils.ts
+++ b/frontend-web/src/core/messages/utils.ts
@@ -1,5 +1,7 @@
import type { AIMessage, Message } from "@langchain/langgraph-sdk";
+import { hasSearchResultPayload } from "@/core/messages/search-results";
+
interface GenericMessageGroup {
type: T;
id: string | undefined;
@@ -141,21 +143,63 @@ export function groupMessages(
}
export function extractTextFromMessage(message: Message) {
- if (typeof message.content === "string") {
- return (
- splitInlineReasoningFromAIMessage(message)?.content ??
- message.content.trim()
- );
+ const payload = extractToolResultPayload(message);
+ if (typeof payload === "string") {
+ return payload;
}
- if (Array.isArray(message.content)) {
- return message.content
- .map((content) => (content.type === "text" ? content.text : ""))
- .join("\n")
- .trim();
+ if (payload && typeof payload === "object") {
+ try {
+ return JSON.stringify(payload);
+ } catch {
+ return "";
+ }
}
return "";
}
+/**
+ * Tool / skill 返回值可能是 JSON 字符串,也可能是结构化对象
+ *(如内网检索:{ success, query, count, results: [{ title, content_preview }] })。
+ */
+export function extractToolResultPayload(
+ message: Message,
+): string | Record | undefined {
+ const { content } = message;
+
+ if (typeof content === "string") {
+ const trimmed = content.trim();
+ return trimmed || undefined;
+ }
+
+ if (content && typeof content === "object" && !Array.isArray(content)) {
+ return content as Record;
+ }
+
+ if (Array.isArray(content)) {
+ const textParts: string[] = [];
+ for (const part of content) {
+ if (!part || typeof part !== "object") {
+ continue;
+ }
+ const block = part as Record;
+ if (block.type === "text" && typeof block.text === "string") {
+ textParts.push(block.text);
+ continue;
+ }
+ if (block.type === "json" && block.json && typeof block.json === "object") {
+ return block.json as Record;
+ }
+ if (hasSearchResultPayload(block)) {
+ return block;
+ }
+ }
+ const joined = textParts.join("\n").trim();
+ return joined || undefined;
+ }
+
+ return undefined;
+}
+
const THINK_TAG_RE = /\s*([\s\S]*?)\s*<\/think>/g;
function splitInlineReasoning(content: string) {
@@ -335,15 +379,37 @@ export function hasSubagent(message: AIMessage) {
return false;
}
-export function findToolCallResult(toolCallId: string, messages: Message[]) {
+export function findToolCallResult(
+ toolCallId: string,
+ messages: Message[],
+ toolName?: string,
+): string | Record | undefined {
for (const message of messages) {
- if (message.type === "tool" && message.tool_call_id === toolCallId) {
- const content = extractTextFromMessage(message);
- if (content) {
- return content;
+ if (message.type !== "tool" || message.tool_call_id !== toolCallId) {
+ continue;
+ }
+ const payload = extractToolResultPayload(message);
+ if (payload !== undefined) {
+ return payload;
+ }
+ }
+
+ // updates 流里偶发缺少 tool_call_id,但 content 已是检索结构
+ if (toolName) {
+ for (const message of messages) {
+ if (message.type !== "tool" || message.name !== toolName) {
+ continue;
+ }
+ if (message.tool_call_id && message.tool_call_id !== toolCallId) {
+ continue;
+ }
+ const payload = extractToolResultPayload(message);
+ if (payload !== undefined && hasSearchResultPayload(payload)) {
+ return payload;
}
}
}
+
return undefined;
}
diff --git a/frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx b/frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx
index 11b7dd23..129b0bea 100644
--- a/frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx
+++ b/frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx
@@ -19,6 +19,8 @@ import { useArticleTypes } from '../../hooks/useArticleTypes';
interface AIWritingFormProps {
onSubmit: (req: AIWritingRequest) => void;
disabled?: boolean;
+ /** workspace 入口:隐藏检索关键词数量、严格模式、目标读者 */
+ hideAdvancedFields?: boolean;
}
interface ModelOption {
@@ -31,7 +33,14 @@ const INPUT_CLASS =
'placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 ' +
'focus-visible:ring-ring disabled:opacity-50';
-export function AIWritingForm({ onSubmit, disabled }: AIWritingFormProps) {
+const DEFAULT_KEYWORD_COUNT = 6;
+const DEFAULT_AUDIENCE = '普通读者';
+
+export function AIWritingForm({
+ onSubmit,
+ disabled,
+ hideAdvancedFields = false,
+}: AIWritingFormProps) {
const [intent, setIntent] = useState('');
const [articleType, setArticleType] = useState('analysis');
const [audience, setAudience] = useState('普通读者');
@@ -110,14 +119,18 @@ export function AIWritingForm({ onSubmit, disabled }: AIWritingFormProps) {
e.preventDefault();
if (!intent.trim()) return;
const wordCount = Math.max(100, parseInt(wordCountInput, 10) || 800);
- const keywordCount = Math.max(1, Math.min(10, parseInt(keywordCountInput, 10) || 6));
+ const keywordCount = hideAdvancedFields
+ ? DEFAULT_KEYWORD_COUNT
+ : Math.max(1, Math.min(10, parseInt(keywordCountInput, 10) || DEFAULT_KEYWORD_COUNT));
onSubmit({
userIntent: intent.trim(),
articleType,
- targetAudience: audience.trim() || '普通读者',
+ targetAudience: hideAdvancedFields
+ ? DEFAULT_AUDIENCE
+ : audience.trim() || DEFAULT_AUDIENCE,
wordCountTarget: wordCount,
keywordCount,
- writingMode: strictMode ? 'strict' : 'loose',
+ writingMode: hideAdvancedFields ? 'loose' : strictMode ? 'strict' : 'loose',
modelName: modelName || undefined,
});
}
@@ -173,53 +186,57 @@ export function AIWritingForm({ onSubmit, disabled }: AIWritingFormProps) {
-