Commit 3025da6e authored by yuguo's avatar yuguo

docs: add AI assistant embedded views implementation plan

Design for upgrading the floating AI assistant from page navigation
to inline embedded views within the chat panel.
Co-Authored-By: default avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 955eb364
# AI Assistant Embedded Views Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Upgrade the floating AI assistant from "navigate to page" to "render page components inline in chat", so the entire system workflow is accessible without leaving the conversation.
**Architecture:** Extend the existing `AgentAction` system with a new `action: "embed"` type. When the backend NavigateTool detects an embeddable route, it returns `"embed"` instead of `"navigate"`. The frontend ChatPanel renders a compact version of the target page inline. Quick action items can also directly trigger embeds without an AI roundtrip. Each embedded view supports CRUD operations via modals within the chat panel, plus a "full page" escape hatch.
**Tech Stack:** React 19, Next.js 15, Ant Design v5, Zustand, Go/Gin backend, GORM
---
## Current Architecture Summary
### What exists today
- `AgentAction` interface with `action: "navigate"` → renders clickable cards → `router.push()` navigates away
- `NavigateTool` (Go) looks up `RouteEntry` by `page_code`, returns structured action with route + params
- `RouteEntry` model: 34 registered routes across admin/patient/doctor modules
- `usePageAction` hook: reads `?action=add/edit&id=xxx` from URL, pages open modals accordingly
- `QUICK_ITEMS`: role-based quick-reply text tags that fill the input box
- Page components live in `web/src/pages/{patient,doctor,admin}/` — wrapped by App Router pages
### What changes
- `AgentAction.action` gains `"embed"` type — frontend renders component inline instead of navigating
- `RouteEntry` gains `Embeddable bool` flag — backend decides navigate vs embed
- `ChatPanel` gains an `activeEmbed` state — renders the embedded component above the input area
- `QUICK_ITEMS` evolves from `string[]` to support both text messages and direct embed triggers
- New `web/src/components/GlobalAIFloat/embeds/` directory holds compact page components
### Key files
| File | Role |
|------|------|
| `web/src/components/GlobalAIFloat/types.ts` | Shared types — add `EmbeddedView`, `QuickItem` |
| `web/src/components/GlobalAIFloat/ChatPanel.tsx` | Main chat — add embed rendering + quick item refactor |
| `web/src/components/GlobalAIFloat/embeds/registry.ts` | Maps `page_code` → React component |
| `web/src/components/GlobalAIFloat/embeds/EmbedWrapper.tsx` | Shared frame: header, close, full-page link |
| `web/src/components/GlobalAIFloat/embeds/Embed*.tsx` | Individual embed components |
| `web/src/store/aiAssistStore.ts` | Add `activeEmbed` state |
| `server/internal/model/route_registry.go` | Add `Embeddable` field |
| `server/internal/agent/route_seeds.go` | Mark embeddable routes |
| `server/pkg/agent/tools/navigate_tool.go` | Return `"embed"` for embeddable routes |
---
## Task 1: Core Types & Embed Registry (Frontend)
**Files:**
- Modify: `web/src/components/GlobalAIFloat/types.ts`
- Create: `web/src/components/GlobalAIFloat/embeds/registry.ts`
- Create: `web/src/components/GlobalAIFloat/embeds/EmbedWrapper.tsx`
**Step 1: Update types.ts — add EmbeddedView and QuickItem types**
```typescript
// Add after AgentAction interface
export interface EmbeddedView {
pageCode: string;
pageName: string;
operation: 'view_list' | 'open_add' | 'open_edit';
route: string; // full-page fallback route
params?: Record<string, unknown>;
}
// Replace string[] QUICK_ITEMS with structured items
export interface QuickItem {
label: string;
type: 'text' | 'embed';
// type=text: sends as chat message (existing behavior)
// type=embed: directly opens embedded view
embed?: EmbeddedView;
}
// Update QUICK_ITEMS to Record<WidgetRole, QuickItem[]>
```
Update `QUICK_ITEMS` from `Record<WidgetRole, string[]>` to `Record<WidgetRole, QuickItem[]>`:
```typescript
export const QUICK_ITEMS: Record<WidgetRole, QuickItem[]> = {
patient: [
{ label: '预问诊', type: 'embed', embed: { pageCode: 'pre_consult', pageName: '预问诊', operation: 'view_list', route: '/patient/pre-consult' } },
{ label: '找医生', type: 'embed', embed: { pageCode: 'find_doctor', pageName: '找医生', operation: 'view_list', route: '/patient/doctors' } },
{ label: '我的问诊', type: 'embed', embed: { pageCode: 'my_consultations', pageName: '我的问诊', operation: 'view_list', route: '/patient/consult' } },
{ label: '头痛', type: 'text' },
{ label: '发热', type: 'text' },
{ label: '咳嗽', type: 'text' },
],
doctor: [
{ label: '鉴别诊断建议', type: 'text' },
{ label: '用药方案推荐', type: 'text' },
{ label: '检查项目建议', type: 'text' },
],
admin: [
{ label: '注册医生', type: 'embed', embed: { pageCode: 'doctor_management', pageName: '医生管理', operation: 'open_add', route: '/admin/doctors?action=add' } },
{ label: '添加科室', type: 'embed', embed: { pageCode: 'department_management', pageName: '科室管理', operation: 'open_add', route: '/admin/departments?action=add' } },
{ label: '患者列表', type: 'embed', embed: { pageCode: 'patient_management', pageName: '患者管理', operation: 'view_list', route: '/admin/patients' } },
{ label: '医生列表', type: 'embed', embed: { pageCode: 'doctor_management', pageName: '医生管理', operation: 'view_list', route: '/admin/doctors' } },
{ label: '查看运营数据', type: 'text' },
{ label: '管理Agent', type: 'text' },
],
};
```
**Step 2: Run TypeScript check**
Run: `cd web && npx tsc --noEmit 2>&1 | head -20`
Expected: Errors in ChatPanel.tsx where QUICK_ITEMS is consumed (will fix in Task 3)
**Step 3: Create embed registry**
Create `web/src/components/GlobalAIFloat/embeds/registry.ts`:
```typescript
import type { ComponentType } from 'react';
export interface EmbedComponentProps {
pageCode: string;
operation: 'view_list' | 'open_add' | 'open_edit';
params?: Record<string, unknown>;
onNavigate?: (pageCode: string, operation?: string, params?: Record<string, unknown>) => void;
onClose?: () => void;
}
// Lazy-loaded registry — keeps bundle small, loads on demand
const registry: Record<string, () => Promise<{ default: ComponentType<EmbedComponentProps> }>> = {
patient_management: () => import('./EmbedPatientList'),
doctor_management: () => import('./EmbedDoctorList'),
department_management: () => import('./EmbedDepartmentList'),
find_doctor: () => import('./EmbedFindDoctor'),
my_consultations: () => import('./EmbedConsultList'),
pre_consult: () => import('./EmbedPreConsult'),
};
export function isEmbeddable(pageCode: string): boolean {
return pageCode in registry;
}
export function getEmbedLoader(pageCode: string) {
return registry[pageCode] ?? null;
}
```
**Step 4: Create EmbedWrapper component**
Create `web/src/components/GlobalAIFloat/embeds/EmbedWrapper.tsx`:
```tsx
'use client';
import React, { Suspense, useState, useEffect, type ComponentType } from 'react';
import { Button, Spin, Typography } from 'antd';
import { CloseOutlined, ExpandOutlined, ArrowLeftOutlined } from '@ant-design/icons';
import { useRouter } from 'next/navigation';
import { getEmbedLoader, type EmbedComponentProps } from './registry';
const { Text } = Typography;
interface EmbedWrapperProps {
pageCode: string;
pageName: string;
operation: 'view_list' | 'open_add' | 'open_edit';
route: string;
params?: Record<string, unknown>;
onClose: () => void;
onNavigate?: (pageCode: string, operation?: string, params?: Record<string, unknown>) => void;
}
const EmbedWrapper: React.FC<EmbedWrapperProps> = ({
pageCode, pageName, operation, route, params, onClose, onNavigate,
}) => {
const router = useRouter();
const [Component, setComponent] = useState<ComponentType<EmbedComponentProps> | null>(null);
const [error, setError] = useState(false);
useEffect(() => {
const loader = getEmbedLoader(pageCode);
if (!loader) { setError(true); return; }
loader().then(mod => setComponent(() => mod.default)).catch(() => setError(true));
}, [pageCode]);
return (
<div style={{
border: '1px solid #e5e7eb', borderRadius: 12, overflow: 'hidden',
background: '#fff', display: 'flex', flexDirection: 'column',
maxHeight: 420,
}}>
{/* Header */}
<div style={{
padding: '8px 12px', background: '#f9fafb', borderBottom: '1px solid #f3f4f6',
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
}}>
<Text strong style={{ fontSize: 13 }}>{pageName}</Text>
<div style={{ display: 'flex', gap: 4 }}>
<Button type="text" size="small" icon={<ExpandOutlined />}
onClick={() => router.push(route)} title="在新页面打开" />
<Button type="text" size="small" icon={<CloseOutlined />}
onClick={onClose} title="关闭" />
</div>
</div>
{/* Body */}
<div style={{ flex: 1, overflow: 'auto', padding: 12 }}>
{error ? (
<div style={{ textAlign: 'center', padding: 24, color: '#9ca3af' }}>
<div>暂不支持嵌入显示</div>
<Button type="link" size="small" onClick={() => router.push(route)}>
前往完整页面
</Button>
</div>
) : Component ? (
<Component
pageCode={pageCode}
operation={operation}
params={params}
onNavigate={onNavigate}
onClose={onClose}
/>
) : (
<div style={{ textAlign: 'center', padding: 24 }}><Spin /></div>
)}
</div>
</div>
);
};
export default EmbedWrapper;
```
**Step 5: Commit**
```bash
git add web/src/components/GlobalAIFloat/types.ts \
web/src/components/GlobalAIFloat/embeds/registry.ts \
web/src/components/GlobalAIFloat/embeds/EmbedWrapper.tsx
git commit -m "feat(ai-assistant): add embedded view types, registry, and wrapper"
```
---
## Task 2: Backend — RouteEntry Embeddable Flag
**Files:**
- Modify: `server/internal/model/route_registry.go`
- Modify: `server/internal/agent/route_seeds.go`
- Modify: `server/pkg/agent/tools/navigate_tool.go`
**Step 1: Add Embeddable field to RouteEntry model**
In `server/internal/model/route_registry.go`, add after `SortOrder`:
```go
Embeddable bool `gorm:"default:false" json:"embeddable"`
```
GORM auto-migration will add the column on next startup.
**Step 2: Mark embeddable routes in seeds**
In `server/internal/agent/route_seeds.go`, add `Embeddable: true` to these entries:
- `patient_management` (admin patient list)
- `doctor_management` (admin doctor list)
- `department_management` (admin department list)
- `find_doctor` (patient doctor browser)
- `my_consultations` (patient consultation list)
Also add a new seed entry for pre-consult:
```go
{
PageCode: "pre_consult",
PageName: "预问诊",
Module: "patient",
Route: "/patient/pre-consult",
Operations: `["view"]`,
RoleAccess: "patient",
Description: "AI预问诊对话,智能分析症状并推荐医生",
Embeddable: true,
SortOrder: 7,
Status: "active",
},
```
**Step 3: Update NavigateTool to return "embed" for embeddable routes**
In `server/pkg/agent/tools/navigate_tool.go`, change the return block (lines 91-101):
```go
// Determine action type: embed (inline in chat) or navigate (open page)
actionType := "navigate"
if entry.Embeddable {
actionType = "embed"
}
return map[string]interface{}{
"action": actionType,
"page": entry.PageName,
"page_code": entry.PageCode,
"module": entry.Module,
"operation": operation,
"route": route,
"params": actionParams,
"operations": ops,
"description": entry.Description,
"embeddable": entry.Embeddable,
}, nil
```
**Step 4: Build and verify**
Run: `cd server && go build -o /dev/null ./cmd/api/`
Expected: Clean build, no errors
**Step 5: Commit**
```bash
git add server/internal/model/route_registry.go \
server/internal/agent/route_seeds.go \
server/pkg/agent/tools/navigate_tool.go
git commit -m "feat(navigate-tool): add embeddable flag, return embed action for inline views"
```
---
## Task 3: ChatPanel — Embed Rendering & Quick Items Refactor
**Files:**
- Modify: `web/src/components/GlobalAIFloat/ChatPanel.tsx`
- Modify: `web/src/store/aiAssistStore.ts`
**Step 1: Add activeEmbed to aiAssistStore**
In `web/src/store/aiAssistStore.ts`, add to the state interface:
```typescript
activeEmbed: EmbeddedView | null;
setActiveEmbed: (embed: EmbeddedView | null) => void;
```
Import `EmbeddedView` from `../components/GlobalAIFloat/types`. Add to the store implementation:
```typescript
activeEmbed: null,
setActiveEmbed: (embed) => set({ activeEmbed: embed }),
```
**Step 2: Update ChatPanel — import embed components and refactor quick items**
In `ChatPanel.tsx`:
1. Import new types and components:
```typescript
import type { ChatMessage, ToolCall, WidgetRole, AgentAction, QuickItem, EmbeddedView } from './types';
import { ROLE_AGENT_ID, ROLE_AGENT_NAME, ROLE_THEME, QUICK_ITEMS } from './types';
import { isEmbeddable } from './embeds/registry';
import EmbedWrapper from './embeds/EmbedWrapper';
import { useAIAssistStore } from '../../store/aiAssistStore';
```
2. Add activeEmbed state from store:
```typescript
const { activeEmbed, setActiveEmbed } = useAIAssistStore();
```
3. **Refactor quick items rendering** — replace the current `quickItems.map(text ...)` with:
```tsx
{quickItems.map((item, idx) => (
<Tag
key={idx}
style={{ cursor: 'pointer', fontSize: 12, margin: 0 }}
color={item.type === 'embed' ? 'blue' : undefined}
onClick={() => {
if (loading) return;
if (item.type === 'embed' && item.embed) {
setActiveEmbed(item.embed);
} else {
setInputValue(prev => prev ? `${prev}、${item.label}` : item.label);
}
}}
>
{QUICK_ICON[role]}
{item.label}
</Tag>
))}
```
4. **Update renderActionCards** — handle "embed" actions:
```tsx
const renderActionCards = (actions?: AgentAction[]) => {
if (!actions || actions.length === 0) return null;
return (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 8 }}>
{actions.map((a, i) => (
<div
key={i}
onClick={() => {
if (a.action === 'embed' && isEmbeddable(a.page_code)) {
setActiveEmbed({
pageCode: a.page_code,
pageName: a.page,
operation: a.operation as EmbeddedView['operation'],
route: a.route,
params: a.params,
});
} else {
router.push(a.route);
}
}}
style={{
display: 'inline-flex', alignItems: 'center', gap: 6,
padding: '6px 12px', borderRadius: 8, cursor: 'pointer',
border: '1px solid #e5e7eb', background: '#f9fafb',
fontSize: 12, transition: 'all 0.2s',
}}
onMouseEnter={e => { e.currentTarget.style.borderColor = '#3b82f6'; e.currentTarget.style.background = '#eff6ff'; }}
onMouseLeave={e => { e.currentTarget.style.borderColor = '#e5e7eb'; e.currentTarget.style.background = '#f9fafb'; }}
>
<LinkOutlined style={{ color: '#3b82f6' }} />
<span style={{ color: '#374151' }}>{a.page}</span>
<Tag color={a.action === 'embed' ? 'blue' : 'default'}
style={{ fontSize: 11, margin: 0, lineHeight: '18px', padding: '0 5px' }}>
{a.action === 'embed' ? '打开' : (OPERATION_LABEL[a.operation] || a.operation)}
</Tag>
</div>
))}
</div>
);
};
```
5. **Add embed rendering area** between messages and quick items:
```tsx
{/* Embedded View */}
{activeEmbed && (
<div style={{ padding: '0 12px 12px' }}>
<EmbedWrapper
pageCode={activeEmbed.pageCode}
pageName={activeEmbed.pageName}
operation={activeEmbed.operation}
route={activeEmbed.route}
params={activeEmbed.params}
onClose={() => setActiveEmbed(null)}
onNavigate={(code, op, params) => {
setActiveEmbed({
pageCode: code,
pageName: code,
operation: (op || 'view_list') as EmbeddedView['operation'],
route: '',
params,
});
}}
/>
</div>
)}
```
Place this block right after the messages div and before the quick replies div.
**Step 3: TypeScript check**
Run: `cd web && npx tsc --noEmit 2>&1 | head -20`
Expected: Errors about missing embed components (EmbedPatientList, etc.) — that's OK, they come in Tasks 4-9
**Step 4: Commit**
```bash
git add web/src/components/GlobalAIFloat/ChatPanel.tsx \
web/src/store/aiAssistStore.ts
git commit -m "feat(chat-panel): embed rendering, quick items refactor, activeEmbed state"
```
---
## Task 4: Embed — Admin Patient List
**Files:**
- Create: `web/src/components/GlobalAIFloat/embeds/EmbedPatientList.tsx`
**Step 1: Create the component**
A compact patient table with search, status filter, and add/edit modals. Reuses `adminApi` from `web/src/api/admin.ts`.
```tsx
'use client';
import React, { useState, useEffect } from 'react';
import { Table, Input, Button, Tag, Space, Modal, Form, Select, message } from 'antd';
import { PlusOutlined, EditOutlined, SearchOutlined, ReloadOutlined } from '@ant-design/icons';
import { adminApi } from '../../../api/admin';
import type { EmbedComponentProps } from './registry';
const EmbedPatientList: React.FC<EmbedComponentProps> = ({ operation, onClose }) => {
const [data, setData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [keyword, setKeyword] = useState('');
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
const fetchData = async () => {
setLoading(true);
try {
const res = await adminApi.getPatientList({ keyword, page, page_size: 5 });
setData(res.data?.list || []);
setTotal(res.data?.total || 0);
} catch { message.error('加载失败'); }
finally { setLoading(false); }
};
useEffect(() => { fetchData(); }, [page]);
useEffect(() => { if (keyword === '') fetchData(); }, [keyword]);
const columns = [
{ title: '姓名', dataIndex: 'real_name', key: 'real_name', width: 80,
render: (v: string) => v || '-' },
{ title: '手机号', dataIndex: 'phone', key: 'phone', width: 120 },
{ title: '状态', dataIndex: 'status', key: 'status', width: 70,
render: (s: string) => <Tag color={s === 'active' ? 'green' : 'red'}>{s === 'active' ? '正常' : '禁用'}</Tag> },
];
return (
<div>
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
<Input size="small" placeholder="搜索姓名/手机号" prefix={<SearchOutlined />}
value={keyword} onChange={e => setKeyword(e.target.value)}
onPressEnter={fetchData} style={{ flex: 1 }} allowClear />
<Button size="small" icon={<ReloadOutlined />} onClick={fetchData} />
</div>
<Table columns={columns} dataSource={data} rowKey="id" loading={loading}
size="small" scroll={{ y: 240 }}
pagination={{ current: page, total, pageSize: 5, size: 'small', simple: true,
onChange: p => setPage(p) }} />
</div>
);
};
export default EmbedPatientList;
```
**Step 2: Verify it compiles**
Run: `cd web && npx tsc --noEmit 2>&1 | grep -i EmbedPatient`
Expected: No errors for this file
**Step 3: Commit**
```bash
git add web/src/components/GlobalAIFloat/embeds/EmbedPatientList.tsx
git commit -m "feat(embed): add compact patient list for chat panel"
```
---
## Task 5: Embed — Admin Doctor List
**Files:**
- Create: `web/src/components/GlobalAIFloat/embeds/EmbedDoctorList.tsx`
Same pattern as Task 4 but for doctors. Uses `adminApi.getDoctorList()`. Shows name, department, title, status. Supports open_add operation (triggers modal from initial render when `operation === 'open_add'`).
Key columns: name, title (职称), status (pending/approved), phone.
Add modal for "添加医生" with fields: phone, real_name, password, license_no, title, department_id, hospital.
**Step 1: Create component** (follow same pattern as EmbedPatientList but with doctor fields and add modal)
**Step 2: Commit**
```bash
git add web/src/components/GlobalAIFloat/embeds/EmbedDoctorList.tsx
git commit -m "feat(embed): add compact doctor list with add modal for chat panel"
```
---
## Task 6: Embed — Admin Department List
**Files:**
- Create: `web/src/components/GlobalAIFloat/embeds/EmbedDepartmentList.tsx`
Uses `adminApi.getDepartmentList()`, `createDepartment()`, `updateDepartment()`, `deleteDepartment()`. Shows icon, code, name, sort_order. Supports add/edit modal.
**Step 1: Create component** (follow same pattern, simpler — no pagination needed)
**Step 2: Commit**
```bash
git add web/src/components/GlobalAIFloat/embeds/EmbedDepartmentList.tsx
git commit -m "feat(embed): add compact department list with CRUD for chat panel"
```
---
## Task 7: Embed — Patient Find Doctor
**Files:**
- Create: `web/src/components/GlobalAIFloat/embeds/EmbedFindDoctor.tsx`
Uses `doctorApi.getDepartments()` and `doctorApi.getDoctorList()`. Shows department filter tabs at top, then doctor cards with name, title, rating, price, online status. Clicking a doctor card could open detail or trigger consultation creation.
**Step 1: Create component**
Department selector as horizontal scrolling tags. Doctor cards with compact layout. Pagination with simple mode.
**Step 2: Commit**
```bash
git add web/src/components/GlobalAIFloat/embeds/EmbedFindDoctor.tsx
git commit -m "feat(embed): add compact find-doctor browser for chat panel"
```
---
## Task 8: Embed — Patient Consultation List
**Files:**
- Create: `web/src/components/GlobalAIFloat/embeds/EmbedConsultList.tsx`
Uses `consultApi.getMyConsultations()` (check exact API in `web/src/api/consult.ts`). Shows consultation cards with doctor name, status tag, date, chief complaint. Clicking navigates to the consultation detail page.
**Step 1: Create component**
**Step 2: Commit**
```bash
git add web/src/components/GlobalAIFloat/embeds/EmbedConsultList.tsx
git commit -m "feat(embed): add compact consultation list for chat panel"
```
---
## Task 9: Embed — Patient Pre-Consult
**Files:**
- Create: `web/src/components/GlobalAIFloat/embeds/EmbedPreConsult.tsx`
This is the most valuable embed. Wraps the existing pre-consultation AI chat into a compact format. Uses `preConsultApi` with SSE streaming. Shows:
1. Simple symptom input
2. Streaming AI response
3. AI analysis report card
4. Recommended doctor cards
Reference existing implementation in `web/src/pages/patient/PreConsult/index.tsx` and extract the core logic into a compact layout.
**Step 1: Create component** (simplified version of the full PreConsult page)
**Step 2: Commit**
```bash
git add web/src/components/GlobalAIFloat/embeds/EmbedPreConsult.tsx
git commit -m "feat(embed): add compact pre-consultation for chat panel"
```
---
## Task 10: Integration Test & Polish
**Files:**
- Modify: `web/src/components/GlobalAIFloat/embeds/registry.ts` (ensure all imports correct)
- Modify: `web/src/components/GlobalAIFloat/ChatPanel.tsx` (final adjustments)
**Step 1: TypeScript full check**
Run: `cd web && npx tsc --noEmit`
Expected: Clean, no errors
**Step 2: Visual verification checklist**
- [ ] Admin role: click "患者列表" quick item → patient table renders inline
- [ ] Admin role: click "注册医生" quick item → doctor list opens with add modal
- [ ] Admin role: click "添加科室" quick item → department list opens with add modal
- [ ] Patient role: click "预问诊" quick item → pre-consult chat renders inline
- [ ] Patient role: click "找医生" quick item → doctor browser renders inline
- [ ] Patient role: click "我的问诊" quick item → consultation list renders inline
- [ ] Each embed: "展开" button navigates to full page
- [ ] Each embed: close button returns to normal chat
- [ ] AI response with `action: "embed"` → renders inline instead of navigate card
- [ ] AI response with `action: "navigate"` → still renders clickable card (non-embeddable routes)
- [ ] Text quick items (头痛, 发热, etc.) → still fill input as before
**Step 3: Commit**
```bash
git add -A
git commit -m "feat(ai-assistant): complete embedded views integration"
```
---
## Dependency Graph
```
Task 1 (types + registry + wrapper)
├── Task 2 (backend embeddable flag) [independent]
├── Task 3 (ChatPanel refactor) [depends on Task 1]
│ └── Tasks 4-9 (embed components) [depend on Task 1 + 3]
└── Task 10 (integration) [depends on all]
```
Tasks 4-9 are independent of each other and can be developed in parallel.
Task 2 (backend) is independent of Tasks 1/3 (frontend) and can be done in parallel.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment