-
当前对话模型
- {modelsLoading || models.length === 0 ? (
-
- {modelsLoading ? '加载中…' : '暂无模型'}
+
+ {/* 与原型图一致:白色圆角卡片包裹「模型选择行 + 输入框」 */}
+
+
+
+
+
当前对话模型
+ {modelsLoading || models.length === 0 ? (
+
+ {modelsLoading ? '加载中…' : '暂无模型'}
+
+ ) : (
+
+ )}
- ) : (
-
- )}
+
+ 输入想法,按 Enter 直接解析填充
+
+
+
void handleChatSubmit(text)}>
+
+
+
+
+
-
void handleChatSubmit(text)}>
-
-
-
-
-
diff --git a/frontend-web/src/open-canvas/components/ai-writing/WritingWelcome.tsx b/frontend-web/src/open-canvas/components/ai-writing/WritingWelcome.tsx
new file mode 100644
index 00000000..30110ee4
--- /dev/null
+++ b/frontend-web/src/open-canvas/components/ai-writing/WritingWelcome.tsx
@@ -0,0 +1,73 @@
+import { PenTool, Sliders, Sparkles } from 'lucide-react';
+
+/**
+ * AI 写作入口页左栏「欢迎/空状态」屏。
+ *
+ * 仅在写作助手对话还没有任何消息时显示(见 WritingSetupChat)。一旦用户在底栏发出
+ * 第一条消息,就回到正常的对话时间线。
+ */
+
+const STEPS = [
+ {
+ n: '1',
+ title: '对白意图解析',
+ desc: '在底栏用人话随便说出你的想法,AI 会瞬间解析要求并填充右侧配置。',
+ icon: Sparkles,
+ iconClass: 'bg-indigo-50 text-indigo-600 dark:bg-indigo-500/15 dark:text-indigo-300',
+ },
+ {
+ n: '2',
+ title: '参数多端对齐',
+ desc: '右侧配置框即刻对齐所需的体裁、目标行文大纲和字数限制。',
+ icon: Sliders,
+ iconClass: 'bg-amber-50 text-amber-600 dark:bg-amber-500/15 dark:text-amber-300',
+ },
+ {
+ n: '3',
+ title: '深度排版落成',
+ desc: '点击右侧红色创作按钮,精美的行业或创意文档极速铺开。',
+ icon: PenTool,
+ iconClass: 'bg-emerald-50 text-emerald-600 dark:bg-emerald-500/15 dark:text-emerald-300',
+ },
+];
+
+export function WritingWelcome() {
+ return (
+
+
+ {/* 标题 + 介绍 + 三步引导卡片:在区域里垂直居中 */}
+
+
+
+ 欢迎使用「妙笔 AI」智力打字协作工坊
+
+
+ 这是一个专业的自动化写作系统。你只需在下方对话框中
+
+ 输入你的本意(例如:「帮我写一篇 1500 字的新能源汽车报告」)
+
+ ,系统即会自动为你在右侧填充所需的字数、体裁等格式进行对齐;你也可在右侧进行纯人工的参数深度定制,并一键开启 AI 深度协作创作。
+
+
+
+
+ {STEPS.map((step) => (
+
+
+
+
+
+ {step.n}. {step.title}
+
+
{step.desc}
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/frontend-web/src/pages/EmbedRoundtablePage.tsx b/frontend-web/src/pages/EmbedRoundtablePage.tsx
new file mode 100644
index 00000000..2d626389
--- /dev/null
+++ b/frontend-web/src/pages/EmbedRoundtablePage.tsx
@@ -0,0 +1,121 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { useSearchParams } from "next/navigation";
+
+import { useTheme } from "@/contexts/ThemeContext";
+import { loginByUsername } from "@/core/auth/api";
+import { setStoredAuth } from "@/core/auth";
+import { readWujieProps, isWujieEnv } from "@/core/embed/wujie";
+import RoundtablePlanningPage from "@/roundtable-planning/pages/RoundtablePlanningPage";
+import { ChatRuntime } from "./WorkspaceRoutes";
+import "@/roundtable-planning/styles/roundtable-planning.css";
+
+const DEFAULT_USERNAME = "guest";
+
+/**
+ * 把宿主传入的主题标识映射成 light/dark。**默认深蓝(dark)**:
+ * 只有明确传 light/红白 才走浅色,其余(dark / dark-blue / 深蓝 / 空)一律深蓝。
+ * 不复用 `resolveEmbedTheme`(它只认严格的 "dark-blue",传 "dark" 会被判成浅色)。
+ */
+function resolveEmbedRoundtableTheme(raw: string): "dark" | "light" {
+ const v = raw.trim().toLowerCase();
+ if (v === "light" || v === "red" || v === "红白") return "light";
+ return "dark";
+}
+
+/**
+ * 圆桌规划嵌入页(独立入口,**与现有 wujie ticket 登录无关**)。
+ *
+ * 用于宿主(如 parallel-agents-vue 的 RoundtableDrawer)用无界把圆桌**主体页面**塞进抽屉:
+ * - **登录**:走「用户名免密登录」(`loginByUsername`),username 来自 `window.$wujie.props.username`
+ * (或 URL `?username=`),不经 LoginPage 的 MaxKey/consumer ticket 流程。
+ * - **无侧边栏**:本路由挂在顶层(`/embed/roundtable`),在 `AppProviders` 之下、`PageRoutes` 之外,
+ * 天然不含页面侧边栏 / workspace 导航,只渲染圆桌主体。
+ * - **任务预填**:taskTitle / taskGoal / chainId 由 `RoundtablePlanningPage` 自身的
+ * `readWujiePrefill()` 直接从 `window.$wujie.props` 读取(抽屉打开即自动跑第一步、按链条进第二步)。
+ */
+export default function EmbedRoundtablePage() {
+ const searchParams = useSearchParams();
+ const { setTheme } = useTheme();
+
+ // username / password / theme:wujie props 优先,URL query 兜底。
+ const wp = readWujieProps();
+ const username =
+ (typeof wp.username === "string" && wp.username.trim()) ||
+ (typeof wp.userName === "string" && wp.userName.trim()) ||
+ searchParams.get("username")?.trim() ||
+ searchParams.get("userName")?.trim() ||
+ DEFAULT_USERNAME;
+ // 共享口令(用户名+密码登录模式):仅当后端开启 username_login.require_password 时校验。
+ // 不 trim —— 口令可能含首尾空格;未传则按「免密」走(后端未开门禁时照常)。
+ const password =
+ (typeof wp.password === "string" && wp.password) ||
+ searchParams.get("password") ||
+ undefined;
+ // 主题色:宿主传入优先;默认「深蓝」= dark 主题(其主色为蓝,oklch hue 259)。
+ const themeParam =
+ (typeof wp.theme === "string" && wp.theme.trim()) ||
+ searchParams.get("theme")?.trim() ||
+ "dark";
+
+ const [authReady, setAuthReady] = useState(false);
+ const [authError, setAuthError] = useState
(null);
+
+ useEffect(() => {
+ setTheme(resolveEmbedRoundtableTheme(themeParam));
+ }, [themeParam, setTheme]);
+
+ useEffect(() => {
+ if (!isWujieEnv()) return;
+ document.documentElement.classList.add("wujie-embed");
+ return () => document.documentElement.classList.remove("wujie-embed");
+ }, []);
+
+ useEffect(() => {
+ let cancelled = false;
+ async function runLogin() {
+ setAuthError(null);
+ try {
+ const auth = await loginByUsername(username, password);
+ if (cancelled) return;
+ setStoredAuth(auth);
+ setAuthReady(true);
+ } catch (error) {
+ if (cancelled) return;
+ setAuthError(error instanceof Error ? error.message : "登录失败");
+ }
+ }
+ void runLogin();
+ return () => {
+ cancelled = true;
+ };
+ }, [username, password]);
+
+ if (authError) {
+ return (
+
+ 登录失败:{authError}
+
+ );
+ }
+
+ if (!authReady) {
+ return (
+
+ 正在进入圆桌…
+
+ );
+ }
+
+ // RoundtablePlanningPage 依赖 SubtasksProvider / ArtifactsProvider / PromptInputProvider
+ // (Step 2 沙箱用 useArtifacts)。ChatRuntime 正是这三者的封装;不套 WorkspaceLayout,
+ // 故无侧边栏,只渲染圆桌主体。
+ return (
+
+
+
+
+
+ );
+}
diff --git a/frontend-web/src/roundtable-planning/api/drafts.ts b/frontend-web/src/roundtable-planning/api/drafts.ts
index 244d97fe..ac9ba0ff 100644
--- a/frontend-web/src/roundtable-planning/api/drafts.ts
+++ b/frontend-web/src/roundtable-planning/api/drafts.ts
@@ -116,6 +116,8 @@ export interface DraftStep3Snapshot {
/** Lightweight metadata row returned by the list endpoint. */
export interface DraftMeta {
id: string;
+ /** 外部任务 id(无界嵌入抽屉传入);非空时草稿按 task_id 共享、不按 user 分权。 */
+ taskId?: string | null;
/** Auto-derived task summary (objective → first user message → timestamp). */
title: string;
furthestStep: 1 | 2 | 3;
@@ -141,6 +143,8 @@ export interface DraftRecord extends DraftMeta {
export interface DraftCreatePayload {
/** Optional stable id so the client can keep one id across create + updates. */
id?: string;
+ /** 外部任务 id(无界嵌入抽屉);设置后草稿按 task_id 共享、不按 user 分权。 */
+ taskId?: string | null;
title: string;
furthestStep: 1 | 2 | 3;
step1: DraftStep1Snapshot;
@@ -152,6 +156,8 @@ export interface DraftCreatePayload {
export interface DraftUpdatePayload {
title?: string;
furthestStep?: 1 | 2 | 3;
+ /** 给已有草稿补绑外部任务 id。 */
+ taskId?: string | null;
step1?: DraftStep1Snapshot;
step2?: DraftStep2Snapshot;
step3?: DraftStep3Snapshot | null;
@@ -173,6 +179,7 @@ async function readJson(res: Response, ctx: string): Promise {
interface ServerDraftMeta {
id: string;
+ task_id?: string | null;
title: string;
furthest_step: number;
created_at: string | null;
@@ -191,6 +198,7 @@ function toMeta(s: ServerDraftMeta): DraftMeta {
const step = Math.min(3, Math.max(1, s.furthest_step || 1)) as 1 | 2 | 3;
return {
id: s.id,
+ taskId: s.task_id ?? null,
title: s.title ?? "",
furthestStep: step,
createdAt: s.created_at ?? "",
@@ -212,6 +220,7 @@ function toRecord(s: ServerDraftFull): DraftRecord {
function fromCreate(p: DraftCreatePayload) {
return {
...(p.id ? { id: p.id } : {}),
+ ...(p.taskId ? { task_id: p.taskId } : {}),
title: p.title,
furthest_step: p.furthestStep,
step1: p.step1,
@@ -224,6 +233,7 @@ function fromUpdate(p: DraftUpdatePayload) {
const out: Record = {};
if (p.title !== undefined) out.title = p.title;
if (p.furthestStep !== undefined) out.furthest_step = p.furthestStep;
+ if (p.taskId !== undefined) out.task_id = p.taskId;
if (p.step1 !== undefined) out.step1 = p.step1;
if (p.step2 !== undefined) out.step2 = p.step2;
if (p.step3 !== undefined) out.step3 = p.step3;
@@ -247,6 +257,17 @@ export async function getDraft(id: string): Promise {
return toRecord(data);
}
+/**
+ * 按外部任务 id 取最新一条草稿(后端忽略 user,不分权)。无则返回 null(404)。
+ * 无界嵌入抽屉打开时用于「该任务有聊天记录则回显最新一条」。
+ */
+export async function getLatestDraftByTask(taskId: string): Promise {
+ const res = await apiFetch(`${BASE()}/by-task/${encodeURIComponent(taskId)}/latest`, { method: "GET" });
+ if (res.status === 404) return null;
+ const data = await readJson(res, "get latest draft by task");
+ return toRecord(data);
+}
+
export async function createDraft(payload: DraftCreatePayload): Promise {
const res = await apiFetch(BASE(), {
method: "POST",
diff --git a/frontend-web/src/roundtable-planning/components/Step3ReportMode.tsx b/frontend-web/src/roundtable-planning/components/Step3ReportMode.tsx
index a4c30959..65abc900 100644
--- a/frontend-web/src/roundtable-planning/components/Step3ReportMode.tsx
+++ b/frontend-web/src/roundtable-planning/components/Step3ReportMode.tsx
@@ -9,7 +9,7 @@
*
* 进入即自动开跑(`useStep3Report.start`),右上角「重新绘制」可重跑。
*/
-import { useEffect, useMemo, useRef, useState } from "react";
+import { useEffect, useMemo, useRef, useState, type FormEvent } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import {
@@ -23,6 +23,7 @@ import {
PanelLeftClose,
PanelLeftOpen,
RefreshCw,
+ SendHorizontal,
Sparkles,
X,
} from "lucide-react";
@@ -197,7 +198,7 @@ export function Step3ReportMode({
);
diff --git a/frontend-web/src/roundtable-planning/hooks/useDraftPersistence.ts b/frontend-web/src/roundtable-planning/hooks/useDraftPersistence.ts
index a0cf8b8a..9968b16d 100644
--- a/frontend-web/src/roundtable-planning/hooks/useDraftPersistence.ts
+++ b/frontend-web/src/roundtable-planning/hooks/useDraftPersistence.ts
@@ -24,8 +24,10 @@ import {
deleteDraft,
deriveDraftName,
getDraft,
+ getLatestDraftByTask,
listDrafts,
updateDraft,
+ type DraftRecord,
type DraftStep2Snapshot,
type DraftStep3Snapshot,
} from "@/roundtable-planning/api/drafts";
@@ -48,6 +50,11 @@ export interface UseDraftPersistenceOptions {
currentStep: number;
setCurrentStep: React.Dispatch