样式修改
This commit is contained in:
1 parent
ab6b6876aa
commit
7f1f34e34f
15 files changed
+536
-100
No files matched your search
@@ -105,6 +105,12 @@ const menuItems: MenuItem[] = [
|
|||||||
icon: List,
|
icon: List,
|
||||||
path: "/page/strategy/light-app/notes",
|
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 },
|
{ id: "light-app-square", label: "应用广场", icon: Layout },
|
||||||
|
|||||||
@@ -463,7 +463,7 @@ interface CoTReasoningStep extends GenericCoTStep<"reasoning"> {
|
|||||||
interface CoTToolCallStep extends GenericCoTStep<"toolCall"> {
|
interface CoTToolCallStep extends GenericCoTStep<"toolCall"> {
|
||||||
name: string;
|
name: string;
|
||||||
args: Record<string, unknown>;
|
args: Record<string, unknown>;
|
||||||
result?: string;
|
result?: string | Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type CoTStep = CoTReasoningStep | CoTToolCallStep;
|
type CoTStep = CoTReasoningStep | CoTToolCallStep;
|
||||||
@@ -495,12 +495,22 @@ function convertToSteps(messages: Message[]): CoTStep[] {
|
|||||||
};
|
};
|
||||||
const toolCallId = tool_call.id;
|
const toolCallId = tool_call.id;
|
||||||
if (toolCallId) {
|
if (toolCallId) {
|
||||||
const toolCallResult = findToolCallResult(toolCallId, messages);
|
const toolCallResult = findToolCallResult(
|
||||||
if (toolCallResult) {
|
toolCallId,
|
||||||
try {
|
messages,
|
||||||
const json = JSON.parse(toolCallResult);
|
tool_call.name,
|
||||||
step.result = json;
|
);
|
||||||
} catch {
|
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;
|
step.result = toolCallResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
ChevronDown,
|
||||||
Download,
|
Download,
|
||||||
FileJson,
|
FileJson,
|
||||||
FileText,
|
FileText,
|
||||||
@@ -63,6 +64,15 @@ export function RecentChatList() {
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
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 } =
|
const { thread_id: threadIdFromPath, agent_id: agentIdFromPath } =
|
||||||
useParams<{
|
useParams<{
|
||||||
thread_id: string;
|
thread_id: string;
|
||||||
@@ -161,12 +171,24 @@ export function RecentChatList() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SidebarGroup className="px-2 pt-3 mt-2 border-t border-sidebar-border/60">
|
<SidebarGroup className="px-2 pt-3 mt-2 border-t border-sidebar-border/60">
|
||||||
<SidebarGroupLabel className="text-muted-foreground/70 px-3 mb-1 text-[10px] font-semibold tracking-[0.12em] uppercase">
|
<SidebarGroupLabel
|
||||||
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true"
|
className="text-muted-foreground/70 px-3 mb-1 text-[10px] font-semibold tracking-[0.12em] uppercase cursor-pointer select-none flex items-center justify-between hover:text-muted-foreground transition-colors"
|
||||||
? t.sidebar.recentChats
|
onClick={toggleCollapsed}
|
||||||
: t.sidebar.demoChats}
|
>
|
||||||
|
<span>
|
||||||
|
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true"
|
||||||
|
? t.sidebar.recentChats
|
||||||
|
: t.sidebar.demoChats}
|
||||||
|
</span>
|
||||||
|
<ChevronDown
|
||||||
|
className={`w-3 h-3 shrink-0 transition-transform duration-200 ${collapsed ? "-rotate-90" : ""}`}
|
||||||
|
/>
|
||||||
</SidebarGroupLabel>
|
</SidebarGroupLabel>
|
||||||
<SidebarGroupContent className="group-data-[collapsible=icon]:pointer-events-none group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0">
|
<SidebarGroupContent
|
||||||
|
className={`group-data-[collapsible=icon]:pointer-events-none group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 overflow-hidden transition-all duration-200 ${
|
||||||
|
collapsed ? "max-h-0 opacity-0 pointer-events-none" : "max-h-[800px] opacity-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<div className="flex w-full flex-col gap-0.5">
|
<div className="flex w-full flex-col gap-0.5">
|
||||||
{visibleThreads.map((thread) => {
|
{visibleThreads.map((thread) => {
|
||||||
|
|||||||
@@ -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<typeof useRouter>,
|
||||||
|
) {
|
||||||
|
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 (
|
||||||
|
<SidebarGroup className="px-2 pt-3 mt-0 border-t border-sidebar-border/60">
|
||||||
|
<SidebarGroupLabel
|
||||||
|
className="text-muted-foreground/70 px-3 mb-1 text-[10px] font-semibold tracking-[0.12em] uppercase cursor-pointer select-none flex items-center justify-between hover:text-muted-foreground transition-colors"
|
||||||
|
onClick={toggleCollapsed}
|
||||||
|
>
|
||||||
|
<span>最近写作</span>
|
||||||
|
<ChevronDown
|
||||||
|
className={`w-3 h-3 shrink-0 transition-transform duration-200 ${collapsed ? "-rotate-90" : ""}`}
|
||||||
|
/>
|
||||||
|
</SidebarGroupLabel>
|
||||||
|
<SidebarGroupContent
|
||||||
|
className={`group-data-[collapsible=icon]:pointer-events-none group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 overflow-hidden transition-all duration-200 ${
|
||||||
|
collapsed ? "max-h-0 opacity-0 pointer-events-none" : "max-h-[600px] opacity-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<SidebarMenu>
|
||||||
|
<div className="flex w-full flex-col gap-0.5">
|
||||||
|
{loading && sessions.length === 0 ? (
|
||||||
|
<p className="px-3 py-1 text-[12px] text-muted-foreground">加载中…</p>
|
||||||
|
) : (
|
||||||
|
sessions.slice(0, 10).map((session) => {
|
||||||
|
const { label, color } = STATUS_LABELS[session.status] ?? {
|
||||||
|
label: session.status,
|
||||||
|
color: "text-muted-foreground",
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<SidebarMenuItem key={session.id} className="group/side-menu-item">
|
||||||
|
<SidebarMenuButton
|
||||||
|
className={
|
||||||
|
"rounded-lg h-auto py-1.5 px-3 text-muted-foreground " +
|
||||||
|
"hover:text-foreground hover:bg-sidebar-accent/60"
|
||||||
|
}
|
||||||
|
onClick={() => openSession(session, isOnPage, router)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-start min-w-0 w-full">
|
||||||
|
<span className="text-[13px] truncate w-full leading-snug">
|
||||||
|
{session.title}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-1 mt-0.5">
|
||||||
|
<span className={cn("text-[10px] font-medium", color)}>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] text-muted-foreground">·</span>
|
||||||
|
<span className="text-[10px] text-muted-foreground">
|
||||||
|
{formatRelative(session.updated_at)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</SidebarGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import { useTheme } from "@/contexts/ThemeContext";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { RecentChatList } from "./recent-chat-list";
|
import { RecentChatList } from "./recent-chat-list";
|
||||||
|
import { RecentWritingList } from "./recent-writing-list";
|
||||||
import { WorkspaceHeader } from "./workspace-header";
|
import { WorkspaceHeader } from "./workspace-header";
|
||||||
import { WorkspaceNavChatList } from "./workspace-nav-chat-list";
|
import { WorkspaceNavChatList } from "./workspace-nav-chat-list";
|
||||||
import { WorkspaceNavMenu } from "./workspace-nav-menu";
|
import { WorkspaceNavMenu } from "./workspace-nav-menu";
|
||||||
@@ -45,6 +46,7 @@ export function WorkspaceSidebar({
|
|||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<WorkspaceNavChatList showCollapseTrigger={compactChrome} />
|
<WorkspaceNavChatList showCollapseTrigger={compactChrome} />
|
||||||
{isSidebarOpen && <RecentChatList />}
|
{isSidebarOpen && <RecentChatList />}
|
||||||
|
{isSidebarOpen && <RecentWritingList />}
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
{!compactChrome && (
|
{!compactChrome && (
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
|
|||||||
@@ -277,9 +277,14 @@ function normalizeSearchResultItem(raw: unknown): SearchResultItem | null {
|
|||||||
|
|
||||||
const layers = collectNestedRecords(record);
|
const layers = collectNestedRecords(record);
|
||||||
|
|
||||||
|
const indexLabel = pickString(record.index, record.idx, record.rank, record.no);
|
||||||
|
|
||||||
let title =
|
let title =
|
||||||
pickFromLayers(layers, TITLE_ALIASES) ??
|
pickFromLayers(layers, TITLE_ALIASES) ??
|
||||||
pickByKeyPattern(layers, TITLE_KEY_PATTERN, { maxLength: 200 });
|
pickByKeyPattern(layers, TITLE_KEY_PATTERN, { maxLength: 200 });
|
||||||
|
if (!title && indexLabel) {
|
||||||
|
title = `结果 ${indexLabel}`;
|
||||||
|
}
|
||||||
let content =
|
let content =
|
||||||
pickFromLayers(layers, CONTENT_ALIASES) ??
|
pickFromLayers(layers, CONTENT_ALIASES) ??
|
||||||
pickByKeyPattern(layers, CONTENT_KEY_PATTERN, { excludeUrl: true });
|
pickByKeyPattern(layers, CONTENT_KEY_PATTERN, { excludeUrl: true });
|
||||||
@@ -313,7 +318,8 @@ function normalizeSearchResultItem(raw: unknown): SearchResultItem | null {
|
|||||||
title = pickShortTitleFallback(layers, reserved);
|
title = pickShortTitleFallback(layers, reserved);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!title && !content && !url) {
|
// 内网检索可能只有 index + 空 title/content_preview,仍展示占位卡片
|
||||||
|
if (!title && !content && !url && !indexLabel) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { AIMessage, Message } from "@langchain/langgraph-sdk";
|
import type { AIMessage, Message } from "@langchain/langgraph-sdk";
|
||||||
|
|
||||||
|
import { hasSearchResultPayload } from "@/core/messages/search-results";
|
||||||
|
|
||||||
interface GenericMessageGroup<T = string> {
|
interface GenericMessageGroup<T = string> {
|
||||||
type: T;
|
type: T;
|
||||||
id: string | undefined;
|
id: string | undefined;
|
||||||
@@ -141,21 +143,63 @@ export function groupMessages<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function extractTextFromMessage(message: Message) {
|
export function extractTextFromMessage(message: Message) {
|
||||||
if (typeof message.content === "string") {
|
const payload = extractToolResultPayload(message);
|
||||||
return (
|
if (typeof payload === "string") {
|
||||||
splitInlineReasoningFromAIMessage(message)?.content ??
|
return payload;
|
||||||
message.content.trim()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (Array.isArray(message.content)) {
|
if (payload && typeof payload === "object") {
|
||||||
return message.content
|
try {
|
||||||
.map((content) => (content.type === "text" ? content.text : ""))
|
return JSON.stringify(payload);
|
||||||
.join("\n")
|
} catch {
|
||||||
.trim();
|
return "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool / skill 返回值可能是 JSON 字符串,也可能是结构化对象
|
||||||
|
*(如内网检索:{ success, query, count, results: [{ title, content_preview }] })。
|
||||||
|
*/
|
||||||
|
export function extractToolResultPayload(
|
||||||
|
message: Message,
|
||||||
|
): string | Record<string, unknown> | 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<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(content)) {
|
||||||
|
const textParts: string[] = [];
|
||||||
|
for (const part of content) {
|
||||||
|
if (!part || typeof part !== "object") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const block = part as Record<string, unknown>;
|
||||||
|
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<string, unknown>;
|
||||||
|
}
|
||||||
|
if (hasSearchResultPayload(block)) {
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const joined = textParts.join("\n").trim();
|
||||||
|
return joined || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const THINK_TAG_RE = /<think>\s*([\s\S]*?)\s*<\/think>/g;
|
const THINK_TAG_RE = /<think>\s*([\s\S]*?)\s*<\/think>/g;
|
||||||
|
|
||||||
function splitInlineReasoning(content: string) {
|
function splitInlineReasoning(content: string) {
|
||||||
@@ -335,15 +379,37 @@ export function hasSubagent(message: AIMessage) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findToolCallResult(toolCallId: string, messages: Message[]) {
|
export function findToolCallResult(
|
||||||
|
toolCallId: string,
|
||||||
|
messages: Message[],
|
||||||
|
toolName?: string,
|
||||||
|
): string | Record<string, unknown> | undefined {
|
||||||
for (const message of messages) {
|
for (const message of messages) {
|
||||||
if (message.type === "tool" && message.tool_call_id === toolCallId) {
|
if (message.type !== "tool" || message.tool_call_id !== toolCallId) {
|
||||||
const content = extractTextFromMessage(message);
|
continue;
|
||||||
if (content) {
|
}
|
||||||
return content;
|
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;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import { useArticleTypes } from '../../hooks/useArticleTypes';
|
|||||||
interface AIWritingFormProps {
|
interface AIWritingFormProps {
|
||||||
onSubmit: (req: AIWritingRequest) => void;
|
onSubmit: (req: AIWritingRequest) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
/** workspace 入口:隐藏检索关键词数量、严格模式、目标读者 */
|
||||||
|
hideAdvancedFields?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ModelOption {
|
interface ModelOption {
|
||||||
@@ -31,7 +33,14 @@ const INPUT_CLASS =
|
|||||||
'placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 ' +
|
'placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 ' +
|
||||||
'focus-visible:ring-ring disabled:opacity-50';
|
'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 [intent, setIntent] = useState('');
|
||||||
const [articleType, setArticleType] = useState('analysis');
|
const [articleType, setArticleType] = useState('analysis');
|
||||||
const [audience, setAudience] = useState('普通读者');
|
const [audience, setAudience] = useState('普通读者');
|
||||||
@@ -110,14 +119,18 @@ export function AIWritingForm({ onSubmit, disabled }: AIWritingFormProps) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!intent.trim()) return;
|
if (!intent.trim()) return;
|
||||||
const wordCount = Math.max(100, parseInt(wordCountInput, 10) || 800);
|
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({
|
onSubmit({
|
||||||
userIntent: intent.trim(),
|
userIntent: intent.trim(),
|
||||||
articleType,
|
articleType,
|
||||||
targetAudience: audience.trim() || '普通读者',
|
targetAudience: hideAdvancedFields
|
||||||
|
? DEFAULT_AUDIENCE
|
||||||
|
: audience.trim() || DEFAULT_AUDIENCE,
|
||||||
wordCountTarget: wordCount,
|
wordCountTarget: wordCount,
|
||||||
keywordCount,
|
keywordCount,
|
||||||
writingMode: strictMode ? 'strict' : 'loose',
|
writingMode: hideAdvancedFields ? 'loose' : strictMode ? 'strict' : 'loose',
|
||||||
modelName: modelName || undefined,
|
modelName: modelName || undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -173,53 +186,57 @@ export function AIWritingForm({ onSubmit, disabled }: AIWritingFormProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
{!hideAdvancedFields && (
|
||||||
<label className="text-xs font-medium text-muted-foreground">检索关键词数量(1~10)</label>
|
<>
|
||||||
<input
|
<div className="space-y-1.5">
|
||||||
type="number"
|
<label className="text-xs font-medium text-muted-foreground">检索关键词数量(1~10)</label>
|
||||||
min={1}
|
<input
|
||||||
max={10}
|
type="number"
|
||||||
step={1}
|
min={1}
|
||||||
className={INPUT_CLASS}
|
max={10}
|
||||||
placeholder="6"
|
step={1}
|
||||||
value={keywordCountInput}
|
className={INPUT_CLASS}
|
||||||
onChange={(e) => setKeywordCountInput(e.target.value)}
|
placeholder="6"
|
||||||
onBlur={handleKeywordCountBlur}
|
value={keywordCountInput}
|
||||||
disabled={disabled}
|
onChange={(e) => setKeywordCountInput(e.target.value)}
|
||||||
/>
|
onBlur={handleKeywordCountBlur}
|
||||||
<p className="text-[11px] text-muted-foreground/70">
|
disabled={disabled}
|
||||||
素材收集专家用几个关键词检索,越多素材越全面但耗时更长。
|
/>
|
||||||
</p>
|
<p className="text-[11px] text-muted-foreground/70">
|
||||||
</div>
|
素材收集专家用几个关键词检索,越多素材越全面但耗时更长。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-start gap-2 rounded-md border bg-muted/20 px-3 py-2">
|
<div className="flex items-start gap-2 rounded-md border bg-muted/20 px-3 py-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="strict-mode"
|
id="strict-mode"
|
||||||
checked={strictMode}
|
checked={strictMode}
|
||||||
onChange={(e) => setStrictMode(e.target.checked)}
|
onChange={(e) => setStrictMode(e.target.checked)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="mt-0.5 size-3.5 shrink-0 accent-primary cursor-pointer"
|
className="mt-0.5 size-3.5 shrink-0 accent-primary cursor-pointer"
|
||||||
/>
|
/>
|
||||||
<label htmlFor="strict-mode" className="cursor-pointer select-none">
|
<label htmlFor="strict-mode" className="cursor-pointer select-none">
|
||||||
<span className="text-xs font-medium text-foreground/80">严格依据素材写作</span>
|
<span className="text-xs font-medium text-foreground/80">严格依据素材写作</span>
|
||||||
<p className="text-[11px] text-muted-foreground/70 mt-0.5 leading-relaxed">
|
<p className="text-[11px] text-muted-foreground/70 mt-0.5 leading-relaxed">
|
||||||
勾选后作家只依据检索素材写作,素材不足以支撑某章节时会暂停询问你;不勾选则可结合通用知识补充论述。
|
勾选后作家只依据检索素材写作,素材不足以支撑某章节时会暂停询问你;不勾选则可结合通用知识补充论述。
|
||||||
</p>
|
</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label className="text-xs font-medium text-muted-foreground">目标读者</label>
|
<label className="text-xs font-medium text-muted-foreground">目标读者</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className={INPUT_CLASS}
|
className={INPUT_CLASS}
|
||||||
placeholder="普通读者 / 行业专家 / 政策决策者…"
|
placeholder="普通读者 / 行业专家 / 政策决策者…"
|
||||||
value={audience}
|
value={audience}
|
||||||
onChange={(e) => setAudience(e.target.value)}
|
onChange={(e) => setAudience(e.target.value)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{models.length > 0 && (
|
{models.length > 0 && (
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { AIWritingSessionSummary } from '../../api/ai-writing-sessions';
|
import type { AIWritingSessionSummary } from '../../api/ai-writing-sessions';
|
||||||
|
|
||||||
const STATUS_LABELS: Record<string, { label: string; color: string }> = {
|
export const STATUS_LABELS: Record<string, { label: string; color: string }> = {
|
||||||
in_progress: { label: '进行中', color: 'text-blue-500' },
|
in_progress: { label: '进行中', color: 'text-blue-500' },
|
||||||
draft_ready: { label: '草稿', color: 'text-amber-500' },
|
draft_ready: { label: '草稿', color: 'text-amber-500' },
|
||||||
review_ready: { label: '审核', color: 'text-purple-500' },
|
review_ready: { label: '审核', color: 'text-purple-500' },
|
||||||
@@ -13,7 +13,7 @@ const STATUS_LABELS: Record<string, { label: string; color: string }> = {
|
|||||||
error: { label: '错误', color: 'text-destructive' },
|
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 ms = Date.now() - new Date(isoStr).getTime();
|
||||||
const mins = Math.floor(ms / 60000);
|
const mins = Math.floor(ms / 60000);
|
||||||
if (mins < 1) return '刚刚';
|
if (mins < 1) return '刚刚';
|
||||||
@@ -25,7 +25,7 @@ function formatRelative(isoStr: string): string {
|
|||||||
return new Date(isoStr).toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
|
return new Date(isoStr).toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SessionItemProps {
|
export interface SessionItemProps {
|
||||||
session: AIWritingSessionSummary;
|
session: AIWritingSessionSummary;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
@@ -33,7 +33,7 @@ interface SessionItemProps {
|
|||||||
onRename: (title: string) => void;
|
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 [editing, setEditing] = useState(false);
|
||||||
const [editValue, setEditValue] = useState(session.title);
|
const [editValue, setEditValue] = useState(session.title);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|||||||
@@ -65,7 +65,11 @@ const STATUS_LABEL: Record<string, string> = {
|
|||||||
error: '出错了',
|
error: '出错了',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AIWritingPanel() {
|
export function AIWritingPanel({
|
||||||
|
hideAdvancedFields = false,
|
||||||
|
}: {
|
||||||
|
hideAdvancedFields?: boolean;
|
||||||
|
} = {}) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { setArtifact } = useCanvasGraphContext();
|
const { setArtifact } = useCanvasGraphContext();
|
||||||
const { state, startWriting, submitIntervention, pauseWriting, resumeWriting, reset } = useAIWriting();
|
const { state, startWriting, submitIntervention, pauseWriting, resumeWriting, reset } = useAIWriting();
|
||||||
@@ -193,6 +197,35 @@ export function AIWritingPanel() {
|
|||||||
reset();
|
reset();
|
||||||
}, [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<AIWritingSessionSummary>).detail;
|
||||||
|
if (session) handleSelectHistoryRef.current(session);
|
||||||
|
}
|
||||||
|
window.addEventListener('ai-writing:open-session', onOpenSession);
|
||||||
|
return () => window.removeEventListener('ai-writing:open-session', onOpenSession);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// ── ⚠️ 临时调试:admin 一键清理所有过期会话 ────────────────────────────────
|
// ── ⚠️ 临时调试:admin 一键清理所有过期会话 ────────────────────────────────
|
||||||
// ai-writing-优化.md §阶段 7 删冗余代码时,本按钮 + 对应 api 一并删除。
|
// ai-writing-优化.md §阶段 7 删冗余代码时,本按钮 + 对应 api 一并删除。
|
||||||
// 仅 admin 能成功调用(后端 _require_admin),普通用户调会拿到 403。
|
// 仅 admin 能成功调用(后端 _require_admin),普通用户调会拿到 403。
|
||||||
@@ -342,7 +375,10 @@ export function AIWritingPanel() {
|
|||||||
<div ref={scrollRef} className="absolute inset-0 overflow-y-auto">
|
<div ref={scrollRef} className="absolute inset-0 overflow-y-auto">
|
||||||
{/* 未开始时显示表单 */}
|
{/* 未开始时显示表单 */}
|
||||||
{isIdle && (
|
{isIdle && (
|
||||||
<AIWritingForm onSubmit={startWriting} />
|
<AIWritingForm
|
||||||
|
onSubmit={startWriting}
|
||||||
|
hideAdvancedFields={hideAdvancedFields}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 意图解析:流式 → 结果卡片承接。
|
{/* 意图解析:流式 → 结果卡片承接。
|
||||||
@@ -353,7 +389,11 @@ export function AIWritingPanel() {
|
|||||||
<IntentStreamingView text={state.intentStreamingText} thinkingText={state.intentThinkingText} />
|
<IntentStreamingView text={state.intentStreamingText} thinkingText={state.intentThinkingText} />
|
||||||
)}
|
)}
|
||||||
{state.parsedIntent && !state.intentStreamingText && (
|
{state.parsedIntent && !state.intentStreamingText && (
|
||||||
<IntentResultCard intent={state.parsedIntent} articleTypes={articleTypes} />
|
<IntentResultCard
|
||||||
|
intent={state.parsedIntent}
|
||||||
|
articleTypes={articleTypes}
|
||||||
|
hideTargetAudience={hideAdvancedFields}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 运行中或完成后显示时间线 */}
|
{/* 运行中或完成后显示时间线 */}
|
||||||
@@ -484,20 +524,26 @@ export function AIWritingPanel() {
|
|||||||
{articleTypes.find((t) => t.key === state.request!.articleType)?.label ?? state.request.articleType}
|
{articleTypes.find((t) => t.key === state.request!.articleType)?.label ?? state.request.articleType}
|
||||||
</span>
|
</span>
|
||||||
</ReqRow>
|
</ReqRow>
|
||||||
<ReqRow label="目标读者">
|
{!hideAdvancedFields && (
|
||||||
<span className="text-muted-foreground">{state.request.targetAudience}</span>
|
<ReqRow label="目标读者">
|
||||||
</ReqRow>
|
<span className="text-muted-foreground">{state.request.targetAudience}</span>
|
||||||
|
</ReqRow>
|
||||||
|
)}
|
||||||
<ReqRow label="目标字数">
|
<ReqRow label="目标字数">
|
||||||
<span className="text-muted-foreground">{state.request.wordCountTarget} 字</span>
|
<span className="text-muted-foreground">{state.request.wordCountTarget} 字</span>
|
||||||
</ReqRow>
|
</ReqRow>
|
||||||
<ReqRow label="检索关键词数量">
|
{!hideAdvancedFields && (
|
||||||
<span className="text-muted-foreground">{state.request.keywordCount} 个</span>
|
<>
|
||||||
</ReqRow>
|
<ReqRow label="检索关键词数量">
|
||||||
<ReqRow label="写作模式">
|
<span className="text-muted-foreground">{state.request.keywordCount} 个</span>
|
||||||
<span className="text-muted-foreground">
|
</ReqRow>
|
||||||
{state.request.writingMode === 'strict' ? '严格依据素材' : '宽松(可结合通用知识)'}
|
<ReqRow label="写作模式">
|
||||||
</span>
|
<span className="text-muted-foreground">
|
||||||
</ReqRow>
|
{state.request.writingMode === 'strict' ? '严格依据素材' : '宽松(可结合通用知识)'}
|
||||||
|
</span>
|
||||||
|
</ReqRow>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{state.request.modelName && (
|
{state.request.modelName && (
|
||||||
<ReqRow label="写作模型">
|
<ReqRow label="写作模型">
|
||||||
<span className="text-muted-foreground">{state.request.modelName}</span>
|
<span className="text-muted-foreground">{state.request.modelName}</span>
|
||||||
|
|||||||
@@ -6,13 +6,18 @@ import type { ArticleType } from '../../api/ai-writing-article-types';
|
|||||||
interface IntentResultCardProps {
|
interface IntentResultCardProps {
|
||||||
intent: ParsedIntent;
|
intent: ParsedIntent;
|
||||||
articleTypes: ArticleType[];
|
articleTypes: ArticleType[];
|
||||||
|
hideTargetAudience?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 意图解析完成后的结构化卡片。展示 LLM 把用户原始意图理解成的四项:
|
* 意图解析完成后的结构化卡片。展示 LLM 把用户原始意图理解成的四项:
|
||||||
* 文章类型 / 目标读者 / 字数 / 精炼意图。在素材收集开始前作为"承上启下"的视觉锚点。
|
* 文章类型 / 目标读者 / 字数 / 精炼意图。在素材收集开始前作为"承上启下"的视觉锚点。
|
||||||
*/
|
*/
|
||||||
export function IntentResultCard({ intent, articleTypes }: IntentResultCardProps) {
|
export function IntentResultCard({
|
||||||
|
intent,
|
||||||
|
articleTypes,
|
||||||
|
hideTargetAudience = false,
|
||||||
|
}: IntentResultCardProps) {
|
||||||
// article_type 后端给的是 key(如 "analysis"),UI 展示用 label(如 "分析报道")
|
// article_type 后端给的是 key(如 "analysis"),UI 展示用 label(如 "分析报道")
|
||||||
const typeLabel =
|
const typeLabel =
|
||||||
articleTypes.find((t) => t.key === intent.articleType)?.label ?? intent.articleType;
|
articleTypes.find((t) => t.key === intent.articleType)?.label ?? intent.articleType;
|
||||||
@@ -28,7 +33,9 @@ export function IntentResultCard({ intent, articleTypes }: IntentResultCardProps
|
|||||||
</div>
|
</div>
|
||||||
<div className="px-3 py-2.5 space-y-1.5 text-xs">
|
<div className="px-3 py-2.5 space-y-1.5 text-xs">
|
||||||
<Row label="文章类型" value={typeLabel} />
|
<Row label="文章类型" value={typeLabel} />
|
||||||
<Row label="目标读者" value={intent.targetAudience} />
|
{!hideTargetAudience && (
|
||||||
|
<Row label="目标读者" value={intent.targetAudience} />
|
||||||
|
)}
|
||||||
<Row label="字数目标" value={`${intent.wordCountTarget} 字`} />
|
<Row label="字数目标" value={`${intent.wordCountTarget} 字`} />
|
||||||
{intent.refinedIntent && (
|
{intent.refinedIntent && (
|
||||||
<Row label="精炼意图" value={intent.refinedIntent} multiline />
|
<Row label="精炼意图" value={intent.refinedIntent} multiline />
|
||||||
|
|||||||
@@ -103,7 +103,11 @@ const DRAFT_ID = 'aw-draft';
|
|||||||
// Match the original canvas page proportions (chat=35, artifact=65)
|
// Match the original canvas page proportions (chat=35, artifact=65)
|
||||||
const DEFAULT_LAYOUT = { [WRITING_ID]: 35, [DRAFT_ID]: 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<GroupImperativeHandle>(null);
|
const layoutRef = useRef<GroupImperativeHandle>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -128,7 +132,7 @@ export default function AIWritingPage() {
|
|||||||
>
|
>
|
||||||
{/* 与原布局保持相同的 flex-1 min-h-0 包裹,确保内部 h-full 正确解析 */}
|
{/* 与原布局保持相同的 flex-1 min-h-0 包裹,确保内部 h-full 正确解析 */}
|
||||||
<div className="flex-1 min-h-0 overflow-hidden">
|
<div className="flex-1 min-h-0 overflow-hidden">
|
||||||
<AIWritingPanel />
|
<AIWritingPanel hideAdvancedFields={hideAdvancedFields} />
|
||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
|
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative h-full w-full overflow-hidden",
|
||||||
|
isDark ? "bg-slate-950" : "bg-white",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* 背景光晕 */}
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute inset-0"
|
||||||
|
style={{
|
||||||
|
background: isDark
|
||||||
|
? "radial-gradient(ellipse 60% 50% at 50% 46%, rgba(59,130,246,0.10) 0%, rgba(139,92,246,0.06) 50%, transparent 70%)"
|
||||||
|
: "radial-gradient(ellipse 60% 50% at 50% 46%, rgba(147,197,253,0.38) 0%, rgba(165,180,252,0.18) 50%, transparent 70%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 主内容 */}
|
||||||
|
<div className="relative flex h-full flex-col items-center justify-center -translate-y-10">
|
||||||
|
<div className="flex w-full max-w-[520px] flex-col items-center gap-4 px-6">
|
||||||
|
|
||||||
|
{/* 笔记图标 */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex h-12 w-10 shrink-0 items-center justify-center rounded shadow-sm",
|
||||||
|
isDark ? "bg-violet-400 text-slate-950" : "bg-[#4d3f74] text-white",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<BookOpen className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 笔记标题 */}
|
||||||
|
<h1
|
||||||
|
className={cn(
|
||||||
|
"text-xl font-semibold tracking-tight",
|
||||||
|
isDark ? "text-slate-100" : "text-slate-900",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
笔记01
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* 新增来源按钮 */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-1.5 rounded-full px-4 py-1 text-sm font-medium transition-colors",
|
||||||
|
isDark
|
||||||
|
? "bg-sky-500/20 text-sky-300 hover:bg-sky-500/30"
|
||||||
|
: "bg-sky-100 text-sky-600 hover:bg-sky-200",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Plus className="h-3.5 w-3.5" />
|
||||||
|
新增来源
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 问答输入框 */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mt-3 flex w-full items-center gap-3 rounded-full px-5 py-3.5",
|
||||||
|
isDark
|
||||||
|
? "bg-slate-800 ring-1 ring-white/10"
|
||||||
|
: "bg-white shadow-sm ring-1 ring-black/5",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Plus
|
||||||
|
className={cn(
|
||||||
|
"h-4 w-4 shrink-0",
|
||||||
|
isDark ? "text-slate-500" : "text-slate-400",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
value={question}
|
||||||
|
onChange={(e) => 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() && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
className={cn(
|
||||||
|
"flex h-7 w-7 shrink-0 items-center justify-center rounded-full transition-colors",
|
||||||
|
isDark
|
||||||
|
? "bg-sky-500 text-white hover:bg-sky-400"
|
||||||
|
: "bg-[#F5222D] text-white hover:bg-[#CF1322]",
|
||||||
|
)}
|
||||||
|
aria-label="发送"
|
||||||
|
>
|
||||||
|
<Send className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ import AdminUserActivityPage from "./AdminUserActivityPage";
|
|||||||
import TagsPage from "./TagsPage";
|
import TagsPage from "./TagsPage";
|
||||||
import LightAppNewNotePage from "./LightAppNewNotePage";
|
import LightAppNewNotePage from "./LightAppNewNotePage";
|
||||||
import LightAppNoteListPage from "./LightAppNoteListPage";
|
import LightAppNoteListPage from "./LightAppNoteListPage";
|
||||||
|
import LightAppNoteQAPage from "./LightAppNoteQAPage";
|
||||||
|
|
||||||
export default function StrategyRoutes() {
|
export default function StrategyRoutes() {
|
||||||
return (
|
return (
|
||||||
@@ -41,6 +42,7 @@ export default function StrategyRoutes() {
|
|||||||
{/* 轻应用 */}
|
{/* 轻应用 */}
|
||||||
<Route path="light-app/new-note" element={<LightAppNewNotePage />} />
|
<Route path="light-app/new-note" element={<LightAppNewNotePage />} />
|
||||||
<Route path="light-app/notes" element={<LightAppNoteListPage />} />
|
<Route path="light-app/notes" element={<LightAppNoteListPage />} />
|
||||||
|
<Route path="light-app/note-qa" element={<LightAppNoteQAPage />} />
|
||||||
|
|
||||||
{/* 对话 */}
|
{/* 对话 */}
|
||||||
<Route path="chats" element={<ChatsPage />} />
|
<Route path="chats" element={<ChatsPage />} />
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ export default function WorkspaceRoutes({
|
|||||||
path="ai-writing"
|
path="ai-writing"
|
||||||
element={
|
element={
|
||||||
<WorkspaceLayout {...layoutProps}>
|
<WorkspaceLayout {...layoutProps}>
|
||||||
<AIWritingPage />
|
<AIWritingPage hideAdvancedFields />
|
||||||
</WorkspaceLayout>
|
</WorkspaceLayout>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in new issue
Block a user