From 7f1f34e34fcb06523bf8298adf7e784d4fc9f134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Fri, 22 May 2026 21:41:26 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend-web/src/components/page-sidebar.tsx | 6 + .../workspace/messages/message-group.tsx | 24 +++- .../components/workspace/recent-chat-list.tsx | 32 ++++- .../workspace/recent-writing-list.tsx | 122 +++++++++++++++++ .../workspace/workspace-sidebar.tsx | 2 + .../src/core/messages/search-results.ts | 8 +- frontend-web/src/core/messages/utils.ts | 96 ++++++++++--- .../components/ai-writing/AIWritingForm.tsx | 115 +++++++++------- .../ai-writing/AIWritingHistoryDropdown.tsx | 8 +- .../components/ai-writing/AIWritingPanel.tsx | 74 ++++++++-- .../ai-writing/IntentResultCard.tsx | 11 +- .../src/open-canvas/pages/AIWritingPage.tsx | 8 +- frontend-web/src/pages/LightAppNoteQAPage.tsx | 126 ++++++++++++++++++ frontend-web/src/pages/StrategyRoutes.tsx | 2 + frontend-web/src/pages/WorkspaceRoutes.tsx | 2 +- 15 files changed, 536 insertions(+), 100 deletions(-) create mode 100644 frontend-web/src/components/workspace/recent-writing-list.tsx create mode 100644 frontend-web/src/pages/LightAppNoteQAPage.tsx diff --git a/frontend-web/src/components/page-sidebar.tsx b/frontend-web/src/components/page-sidebar.tsx index 42dccac5..ff9b4045 100644 --- a/frontend-web/src/components/page-sidebar.tsx +++ b/frontend-web/src/components/page-sidebar.tsx @@ -105,6 +105,12 @@ const menuItems: MenuItem[] = [ icon: List, path: "/page/strategy/light-app/notes", }, + { + id: "light-app-note-qa", + label: "笔记问答", + icon: MessageSquareCode, + path: "/page/strategy/light-app/note-qa", + }, ], }, { id: "light-app-square", label: "应用广场", icon: Layout }, diff --git a/frontend-web/src/components/workspace/messages/message-group.tsx b/frontend-web/src/components/workspace/messages/message-group.tsx index 63ff8e22..40f5348d 100644 --- a/frontend-web/src/components/workspace/messages/message-group.tsx +++ b/frontend-web/src/components/workspace/messages/message-group.tsx @@ -463,7 +463,7 @@ interface CoTReasoningStep extends GenericCoTStep<"reasoning"> { interface CoTToolCallStep extends GenericCoTStep<"toolCall"> { name: string; args: Record; - result?: string; + result?: string | Record; } type CoTStep = CoTReasoningStep | CoTToolCallStep; @@ -495,12 +495,22 @@ function convertToSteps(messages: Message[]): CoTStep[] { }; const toolCallId = tool_call.id; if (toolCallId) { - const toolCallResult = findToolCallResult(toolCallId, messages); - if (toolCallResult) { - try { - const json = JSON.parse(toolCallResult); - step.result = json; - } catch { + const toolCallResult = findToolCallResult( + toolCallId, + messages, + tool_call.name, + ); + if (toolCallResult !== undefined) { + if (typeof toolCallResult === "string") { + try { + step.result = JSON.parse(toolCallResult) as Record< + string, + unknown + >; + } catch { + step.result = toolCallResult; + } + } else { step.result = toolCallResult; } } diff --git a/frontend-web/src/components/workspace/recent-chat-list.tsx b/frontend-web/src/components/workspace/recent-chat-list.tsx index 85cfe223..c0ed0ed1 100644 --- a/frontend-web/src/components/workspace/recent-chat-list.tsx +++ b/frontend-web/src/components/workspace/recent-chat-list.tsx @@ -1,6 +1,7 @@ "use client"; import { + ChevronDown, Download, FileJson, FileText, @@ -63,6 +64,15 @@ export function RecentChatList() { const { t } = useI18n(); const router = useRouter(); const pathname = usePathname(); + const [collapsed, setCollapsed] = useState( + () => localStorage.getItem("sidebar:recent-chats-collapsed") === "true", + ); + + const toggleCollapsed = () => { + const next = !collapsed; + setCollapsed(next); + localStorage.setItem("sidebar:recent-chats-collapsed", String(next)); + }; const { thread_id: threadIdFromPath, agent_id: agentIdFromPath } = useParams<{ thread_id: string; @@ -161,12 +171,24 @@ export function RecentChatList() { return ( <> - - {env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true" - ? t.sidebar.recentChats - : t.sidebar.demoChats} + + + {env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true" + ? t.sidebar.recentChats + : t.sidebar.demoChats} + + - +
{visibleThreads.map((thread) => { diff --git a/frontend-web/src/components/workspace/recent-writing-list.tsx b/frontend-web/src/components/workspace/recent-writing-list.tsx new file mode 100644 index 00000000..11be8295 --- /dev/null +++ b/frontend-web/src/components/workspace/recent-writing-list.tsx @@ -0,0 +1,122 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { usePathname, useRouter } from "next/navigation"; +import { ChevronDown } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuItem, + SidebarMenuButton, +} from "@/components/ui/sidebar"; +import { + formatRelative, + STATUS_LABELS, +} from "@/open-canvas/components/ai-writing/AIWritingHistoryDropdown"; +import { useAIWritingSessions } from "@/open-canvas/hooks/useAIWritingSessions"; +import type { AIWritingSessionSummary } from "@/open-canvas/api/ai-writing-sessions"; + +const AI_WRITING_PATH = "/page/workspace/ai-writing"; + +function openSession( + session: AIWritingSessionSummary, + isOnPage: boolean, + router: ReturnType, +) { + if (isOnPage) { + // 已在 AI 写作页面,直接通过自定义事件通知面板 + window.dispatchEvent( + new CustomEvent("ai-writing:open-session", { detail: session }), + ); + } else { + // 不在该页面,把 session 存入 sessionStorage 后跳转 + sessionStorage.setItem("ai-writing:pending-session", JSON.stringify(session)); + router.push(AI_WRITING_PATH); + } +} + +export function RecentWritingList() { + const router = useRouter(); + const pathname = usePathname(); + const { sessions, loading, refresh } = useAIWritingSessions(); + const [collapsed, setCollapsed] = useState( + () => localStorage.getItem("sidebar:recent-writing-collapsed") === "true", + ); + + const toggleCollapsed = () => { + const next = !collapsed; + setCollapsed(next); + localStorage.setItem("sidebar:recent-writing-collapsed", String(next)); + }; + + useEffect(() => { + void refresh(); + }, [refresh]); + + if (!loading && sessions.length === 0) return null; + + const isOnPage = pathname === AI_WRITING_PATH; + + return ( + + + 最近写作 + + + + +
+ {loading && sessions.length === 0 ? ( +

加载中…

+ ) : ( + sessions.slice(0, 10).map((session) => { + const { label, color } = STATUS_LABELS[session.status] ?? { + label: session.status, + color: "text-muted-foreground", + }; + return ( + + openSession(session, isOnPage, router)} + > +
+ + {session.title} + +
+ + {label} + + · + + {formatRelative(session.updated_at)} + +
+
+
+
+ ); + }) + )} +
+
+
+
+ ); +} diff --git a/frontend-web/src/components/workspace/workspace-sidebar.tsx b/frontend-web/src/components/workspace/workspace-sidebar.tsx index d2e73183..eeacf4f7 100644 --- a/frontend-web/src/components/workspace/workspace-sidebar.tsx +++ b/frontend-web/src/components/workspace/workspace-sidebar.tsx @@ -12,6 +12,7 @@ import { useTheme } from "@/contexts/ThemeContext"; import { cn } from "@/lib/utils"; import { RecentChatList } from "./recent-chat-list"; +import { RecentWritingList } from "./recent-writing-list"; import { WorkspaceHeader } from "./workspace-header"; import { WorkspaceNavChatList } from "./workspace-nav-chat-list"; import { WorkspaceNavMenu } from "./workspace-nav-menu"; @@ -45,6 +46,7 @@ export function WorkspaceSidebar({ {isSidebarOpen && } + {isSidebarOpen && } {!compactChrome && ( 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) {
-
- - setKeywordCountInput(e.target.value)} - onBlur={handleKeywordCountBlur} - disabled={disabled} - /> -

- 素材收集专家用几个关键词检索,越多素材越全面但耗时更长。 -

-
+ {!hideAdvancedFields && ( + <> +
+ + setKeywordCountInput(e.target.value)} + onBlur={handleKeywordCountBlur} + disabled={disabled} + /> +

+ 素材收集专家用几个关键词检索,越多素材越全面但耗时更长。 +

+
-
- setStrictMode(e.target.checked)} - disabled={disabled} - className="mt-0.5 size-3.5 shrink-0 accent-primary cursor-pointer" - /> - -
+
+ setStrictMode(e.target.checked)} + disabled={disabled} + className="mt-0.5 size-3.5 shrink-0 accent-primary cursor-pointer" + /> + +
-
- - setAudience(e.target.value)} - disabled={disabled} - /> -
+
+ + setAudience(e.target.value)} + disabled={disabled} + /> +
+ + )} {models.length > 0 && (
diff --git a/frontend-web/src/open-canvas/components/ai-writing/AIWritingHistoryDropdown.tsx b/frontend-web/src/open-canvas/components/ai-writing/AIWritingHistoryDropdown.tsx index 6d4c925e..5ba2fd7c 100644 --- a/frontend-web/src/open-canvas/components/ai-writing/AIWritingHistoryDropdown.tsx +++ b/frontend-web/src/open-canvas/components/ai-writing/AIWritingHistoryDropdown.tsx @@ -5,7 +5,7 @@ import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; import type { AIWritingSessionSummary } from '../../api/ai-writing-sessions'; -const STATUS_LABELS: Record = { +export const STATUS_LABELS: Record = { in_progress: { label: '进行中', color: 'text-blue-500' }, draft_ready: { label: '草稿', color: 'text-amber-500' }, review_ready: { label: '审核', color: 'text-purple-500' }, @@ -13,7 +13,7 @@ const STATUS_LABELS: Record = { error: { label: '错误', color: 'text-destructive' }, }; -function formatRelative(isoStr: string): string { +export function formatRelative(isoStr: string): string { const ms = Date.now() - new Date(isoStr).getTime(); const mins = Math.floor(ms / 60000); if (mins < 1) return '刚刚'; @@ -25,7 +25,7 @@ function formatRelative(isoStr: string): string { return new Date(isoStr).toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' }); } -interface SessionItemProps { +export interface SessionItemProps { session: AIWritingSessionSummary; isSelected: boolean; onSelect: () => void; @@ -33,7 +33,7 @@ interface SessionItemProps { onRename: (title: string) => void; } -function SessionItem({ session, isSelected, onSelect, onDelete, onRename }: SessionItemProps) { +export function SessionItem({ session, isSelected, onSelect, onDelete, onRename }: SessionItemProps) { const [editing, setEditing] = useState(false); const [editValue, setEditValue] = useState(session.title); const inputRef = useRef(null); diff --git a/frontend-web/src/open-canvas/components/ai-writing/AIWritingPanel.tsx b/frontend-web/src/open-canvas/components/ai-writing/AIWritingPanel.tsx index de5a4eec..c9b5ba9f 100644 --- a/frontend-web/src/open-canvas/components/ai-writing/AIWritingPanel.tsx +++ b/frontend-web/src/open-canvas/components/ai-writing/AIWritingPanel.tsx @@ -65,7 +65,11 @@ const STATUS_LABEL: Record = { error: '出错了', }; -export function AIWritingPanel() { +export function AIWritingPanel({ + hideAdvancedFields = false, +}: { + hideAdvancedFields?: boolean; +} = {}) { const navigate = useNavigate(); const { setArtifact } = useCanvasGraphContext(); const { state, startWriting, submitIntervention, pauseWriting, resumeWriting, reset } = useAIWriting(); @@ -193,6 +197,35 @@ export function AIWritingPanel() { reset(); }, [reset]); + // ── 侧边栏"最近写作"通信:两种路径 ──────────────────────────────────────── + // 用 ref 包住 handler,保证事件监听器始终能拿到最新版本 + const handleSelectHistoryRef = useRef(handleSelectHistory); + useEffect(() => { + handleSelectHistoryRef.current = handleSelectHistory; + }, [handleSelectHistory]); + + // 路径1:导航进入本页面时,从 sessionStorage 恢复待打开的 session + useEffect(() => { + const raw = sessionStorage.getItem('ai-writing:pending-session'); + if (!raw) return; + sessionStorage.removeItem('ai-writing:pending-session'); + try { + const session = JSON.parse(raw) as AIWritingSessionSummary; + handleSelectHistoryRef.current(session); + } catch { /* ignore */ } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); // 仅挂载时执行一次 + + // 路径2:已在本页面时,监听侧边栏触发的自定义事件 + useEffect(() => { + function onOpenSession(e: Event) { + const session = (e as CustomEvent).detail; + if (session) handleSelectHistoryRef.current(session); + } + window.addEventListener('ai-writing:open-session', onOpenSession); + return () => window.removeEventListener('ai-writing:open-session', onOpenSession); + }, []); + // ── ⚠️ 临时调试:admin 一键清理所有过期会话 ──────────────────────────────── // ai-writing-优化.md §阶段 7 删冗余代码时,本按钮 + 对应 api 一并删除。 // 仅 admin 能成功调用(后端 _require_admin),普通用户调会拿到 403。 @@ -342,7 +375,10 @@ export function AIWritingPanel() {
{/* 未开始时显示表单 */} {isIdle && ( - + )} {/* 意图解析:流式 → 结果卡片承接。 @@ -353,7 +389,11 @@ export function AIWritingPanel() { )} {state.parsedIntent && !state.intentStreamingText && ( - + )} {/* 运行中或完成后显示时间线 */} @@ -484,20 +524,26 @@ export function AIWritingPanel() { {articleTypes.find((t) => t.key === state.request!.articleType)?.label ?? state.request.articleType} - - {state.request.targetAudience} - + {!hideAdvancedFields && ( + + {state.request.targetAudience} + + )} {state.request.wordCountTarget} 字 - - {state.request.keywordCount} 个 - - - - {state.request.writingMode === 'strict' ? '严格依据素材' : '宽松(可结合通用知识)'} - - + {!hideAdvancedFields && ( + <> + + {state.request.keywordCount} 个 + + + + {state.request.writingMode === 'strict' ? '严格依据素材' : '宽松(可结合通用知识)'} + + + + )} {state.request.modelName && ( {state.request.modelName} diff --git a/frontend-web/src/open-canvas/components/ai-writing/IntentResultCard.tsx b/frontend-web/src/open-canvas/components/ai-writing/IntentResultCard.tsx index 316cb59d..1d69ff2f 100644 --- a/frontend-web/src/open-canvas/components/ai-writing/IntentResultCard.tsx +++ b/frontend-web/src/open-canvas/components/ai-writing/IntentResultCard.tsx @@ -6,13 +6,18 @@ import type { ArticleType } from '../../api/ai-writing-article-types'; interface IntentResultCardProps { intent: ParsedIntent; articleTypes: ArticleType[]; + hideTargetAudience?: boolean; } /** * 意图解析完成后的结构化卡片。展示 LLM 把用户原始意图理解成的四项: * 文章类型 / 目标读者 / 字数 / 精炼意图。在素材收集开始前作为"承上启下"的视觉锚点。 */ -export function IntentResultCard({ intent, articleTypes }: IntentResultCardProps) { +export function IntentResultCard({ + intent, + articleTypes, + hideTargetAudience = false, +}: IntentResultCardProps) { // article_type 后端给的是 key(如 "analysis"),UI 展示用 label(如 "分析报道") const typeLabel = articleTypes.find((t) => t.key === intent.articleType)?.label ?? intent.articleType; @@ -28,7 +33,9 @@ export function IntentResultCard({ intent, articleTypes }: IntentResultCardProps
- + {!hideTargetAudience && ( + + )} {intent.refinedIntent && ( diff --git a/frontend-web/src/open-canvas/pages/AIWritingPage.tsx b/frontend-web/src/open-canvas/pages/AIWritingPage.tsx index 99c91440..954c89d0 100644 --- a/frontend-web/src/open-canvas/pages/AIWritingPage.tsx +++ b/frontend-web/src/open-canvas/pages/AIWritingPage.tsx @@ -103,7 +103,11 @@ const DRAFT_ID = 'aw-draft'; // Match the original canvas page proportions (chat=35, artifact=65) const DEFAULT_LAYOUT = { [WRITING_ID]: 35, [DRAFT_ID]: 65 }; -export default function AIWritingPage() { +export default function AIWritingPage({ + hideAdvancedFields = false, +}: { + hideAdvancedFields?: boolean; +} = {}) { const layoutRef = useRef(null); return ( @@ -128,7 +132,7 @@ export default function AIWritingPage() { > {/* 与原布局保持相同的 flex-1 min-h-0 包裹,确保内部 h-full 正确解析 */}
- +
diff --git a/frontend-web/src/pages/LightAppNoteQAPage.tsx b/frontend-web/src/pages/LightAppNoteQAPage.tsx new file mode 100644 index 00000000..6ef3f83c --- /dev/null +++ b/frontend-web/src/pages/LightAppNoteQAPage.tsx @@ -0,0 +1,126 @@ +"use client"; + +import { useState } from "react"; +import { BookOpen, Plus, Send } from "lucide-react"; + +import { useTheme } from "@/contexts/ThemeContext"; +import { cn } from "@/lib/utils"; + +export default function LightAppNoteQAPage() { + const { theme } = useTheme(); + const isDark = theme === "dark"; + const [question, setQuestion] = useState(""); + + const handleSubmit = () => { + if (!question.trim()) return; + setQuestion(""); + }; + + return ( +
+ {/* 背景光晕 */} +
+ + {/* 主内容 */} +
+
+ + {/* 笔记图标 */} +
+ +
+ + {/* 笔记标题 */} +

+ 笔记01 +

+ + {/* 新增来源按钮 */} + + + {/* 问答输入框 */} +
+ + setQuestion(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + handleSubmit(); + } + }} + placeholder="开始问答" + className={cn( + "min-w-0 flex-1 bg-transparent text-sm outline-none", + isDark + ? "text-slate-200 placeholder:text-slate-500" + : "text-slate-700 placeholder:text-slate-400", + )} + /> + {question.trim() && ( + + )} +
+
+
+
+ ); +} diff --git a/frontend-web/src/pages/StrategyRoutes.tsx b/frontend-web/src/pages/StrategyRoutes.tsx index 9fb4ffc3..1fe204e5 100644 --- a/frontend-web/src/pages/StrategyRoutes.tsx +++ b/frontend-web/src/pages/StrategyRoutes.tsx @@ -26,6 +26,7 @@ import AdminUserActivityPage from "./AdminUserActivityPage"; import TagsPage from "./TagsPage"; import LightAppNewNotePage from "./LightAppNewNotePage"; import LightAppNoteListPage from "./LightAppNoteListPage"; +import LightAppNoteQAPage from "./LightAppNoteQAPage"; export default function StrategyRoutes() { return ( @@ -41,6 +42,7 @@ export default function StrategyRoutes() { {/* 轻应用 */} } /> } /> + } /> {/* 对话 */} } /> diff --git a/frontend-web/src/pages/WorkspaceRoutes.tsx b/frontend-web/src/pages/WorkspaceRoutes.tsx index 9afd198f..b618b172 100644 --- a/frontend-web/src/pages/WorkspaceRoutes.tsx +++ b/frontend-web/src/pages/WorkspaceRoutes.tsx @@ -182,7 +182,7 @@ export default function WorkspaceRoutes({ path="ai-writing" element={ - + } />