feat(desktop): add all-projects workbench home
This commit is contained in:
@@ -4,41 +4,43 @@
|
||||
|
||||
- Source visual truth: `C:\Users\david\AppData\Local\Temp\codex-clipboard-72c4b101-58e8-44b0-81b4-01abc35a29e6.png`
|
||||
- Implementation screenshot: `test-results/app-home.png`
|
||||
- Full-view viewport: 420 × 820 CSS px at device scale factor 1; implementation image is 420 × 820 px.
|
||||
- Source image: 144 × 343 px. It shows only a narrow icon rail, whereas the implementation contains the requested complete desktop workbench.
|
||||
- State: logged-in light theme, default “全部” project scope, one pending task and one completed task.
|
||||
- Interaction checks: default task board, task edit modal,资料 panel, AI panel, and profile dropdown. Console errors: none.
|
||||
- Viewport: 420 × 820 CSS px at device scale factor 1; implementation image is 420 × 820 px.
|
||||
- Source image: 144 × 343 px and only shows the compact icon-navigation language. It does not define the requested all-projects dashboard.
|
||||
- State: signed-in light theme, “工作台 / 全部项目” selected, one pending task, one completed task, one latest file.
|
||||
- Primary interactions tested: default all-projects homepage, task editor modal, explore view, project picker, project-only right navigation, document panel, AI panel, and profile dropdown. Console errors: none.
|
||||
|
||||
## Evidence
|
||||
|
||||
- Full implementation: `test-results/app-home.png`
|
||||
- Focused icon-rail comparison: `test-results/design-qa-rail-comparison.png`
|
||||
- Left: the source’s 75 × 343 px rail crop.
|
||||
- Right: the implementation’s 58 × 820 px right rail, normalized to 75 × 343 px.
|
||||
- Full current implementation: `test-results/app-home.png`
|
||||
- Earlier focused icon-rail comparison: `test-results/design-qa-rail-comparison.png`
|
||||
|
||||
The focused comparison is intentionally rail-only because the source does not provide a full workbench layout. The implementation places the rail on the right as requested, rather than copying the source’s left placement.
|
||||
The source rail is used only for the compact icon treatment. The latest requested behavior intentionally hides that rail in the all-projects dashboard and displays it only after selecting a specific project.
|
||||
|
||||
## Findings
|
||||
|
||||
No actionable P0, P1, or P2 findings.
|
||||
|
||||
- Fonts and typography: the application uses the existing Inter / Microsoft YaHei stack. Small rail labels remain legible at the 420 px target width and task titles retain stronger hierarchy.
|
||||
- Spacing and layout rhythm: the 48 px header, 46 px project strip, scrollable content column, and 58 px rail leave the task content unobscured. No horizontal overflow was observed.
|
||||
- Colors and visual tokens: neutral rail, blue selection, green completion, and muted metadata provide equivalent compact navigation contrast to the source while preserving the product’s existing Arco blue token.
|
||||
- Image and icon fidelity: the existing product icon is retained; all navigation controls use the installed Arco icon system. No placeholder or hand-drawn visual assets were introduced.
|
||||
- Copy and content: project scope, task state, profile menu, and submenus use the requested Chinese product language.
|
||||
- Fonts and typography: the existing Inter / Microsoft YaHei stack preserves clear hierarchy across compact project tabs, four statistics, task cards, and latest-file metadata.
|
||||
- Spacing and layout rhythm: the all-projects page fills the former rail width after navigation is hidden. At 420 px, the header, tabs, statistics, actions, task lists, and latest files remain within the viewport with no horizontal overflow.
|
||||
- Colors and visual tokens: workbench/explore selection uses the existing blue token, while statistic colors distinguish projects, pending work, files, and plan tasks without overpowering the task list.
|
||||
- Image and icon fidelity: the existing product mark is retained and all controls use the installed Arco icon set; no placeholder or hand-drawn image assets were added.
|
||||
- Copy and content: “工作台”, “探索”, and “全部项目” make scope explicit; the project-only menu contains 概况、计划、资料、AI、计划任务 and no longer exposes 收集箱.
|
||||
|
||||
## Comparison history
|
||||
|
||||
- Current pass: changed the all-projects scope from a flat merged task list to a dedicated dashboard and hid the right rail for that scope. Re-captured `test-results/app-home.png` and verified the requested dashboard sections and task/edit interactions.
|
||||
|
||||
## Follow-up polish
|
||||
|
||||
- [P3] The source has four tall unlabeled icon slots; the implementation has six compact labeled slots because it must expose the requested project views. If the list grows further, group secondary views under “更多”.
|
||||
- [P3] If project count becomes large, the compact project picker can add a search field; the current list is appropriate for the MVP project volume.
|
||||
|
||||
## Implementation checklist
|
||||
|
||||
- [x] Align desktop API fields and request payloads with `web_v1`.
|
||||
- [x] Add theme toggle and user dropdown with profile editing and logout.
|
||||
- [x] Add project scope strip with 全部, previous/next, and create controls.
|
||||
- [x] Move project submenu navigation to the right rail.
|
||||
- [x] Make pending and completed tasks the default editable content.
|
||||
- [x] Verify primary panels and interaction states.
|
||||
- [x] Replace the project strip with workbench, explore, and current-project controls.
|
||||
- [x] Make all-projects the default selection.
|
||||
- [x] Add all-project statistics, pending tasks, project creation, targeted file upload, and latest files.
|
||||
- [x] Hide right navigation for all-projects and explore views.
|
||||
- [x] Remove 收集箱 and rename 周期计划 to 计划任务.
|
||||
- [x] Verify key interaction paths and responsive composition.
|
||||
|
||||
final result: passed
|
||||
|
||||
@@ -42,14 +42,20 @@ await page.waitForSelector('.task-row')
|
||||
await page.screenshot({ path: 'test-results/app-home.png', fullPage: true })
|
||||
const home = await page.evaluate(() => ({
|
||||
width: document.querySelector('.app-shell')?.getBoundingClientRect().width,
|
||||
selectedAll: document.querySelector('.project-pill.active span')?.textContent,
|
||||
selectedAll: document.querySelector('.project-current span')?.textContent,
|
||||
navCount: document.querySelectorAll('.channel-rail button').length,
|
||||
statCount: document.querySelectorAll('.stat-card').length,
|
||||
pending: document.querySelectorAll('.task-row:not(.done)').length,
|
||||
completed: document.querySelectorAll('.task-row.done').length,
|
||||
}))
|
||||
await page.locator('.task-row').first().click()
|
||||
const taskModal = await page.getByText('编辑任务').count()
|
||||
await page.getByRole('button', { name: '取消' }).click()
|
||||
await page.getByRole('button', { name: '探索' }).click()
|
||||
const exploreProjects = await page.locator('.project-explore-card').count()
|
||||
await page.locator('.project-current').click()
|
||||
await page.getByRole('option', { name: '森林项目' }).click()
|
||||
await page.waitForSelector('.channel-rail')
|
||||
await page.locator('.channel-rail button[title="资料"]').click()
|
||||
const documents = await page.locator('.detail-row').count()
|
||||
await page.locator('.channel-rail button[title="AI"]').click()
|
||||
@@ -63,13 +69,15 @@ await browser.close(); await server.close()
|
||||
|
||||
const failures = []
|
||||
if (home.width !== 420) failures.push(`expected 420px shell, got ${home.width}`)
|
||||
if (home.selectedAll !== '全部') failures.push(`default project scope must be 全部, got ${home.selectedAll}`)
|
||||
if (home.navCount !== 6) failures.push(`expected six right rail menus, got ${home.navCount}`)
|
||||
if (home.selectedAll !== '全部项目') failures.push(`default project scope must be 全部项目, got ${home.selectedAll}`)
|
||||
if (home.navCount !== 0) failures.push(`all-projects view must hide the right rail, got ${home.navCount}`)
|
||||
if (home.statCount !== 4) failures.push(`expected four summary cards, got ${home.statCount}`)
|
||||
if (home.pending !== 1 || home.completed !== 1) failures.push(`task sections incorrect: ${JSON.stringify(home)}`)
|
||||
if (taskModal !== 1) failures.push('task editor did not open')
|
||||
if (exploreProjects !== 1) failures.push(`expected one project in explore, got ${exploreProjects}`)
|
||||
if (documents !== 1) failures.push(`expected one document, got ${documents}`)
|
||||
if (experts !== 1) failures.push(`expected one AI expert, got ${experts}`)
|
||||
if (profileMenu !== 1) failures.push('profile menu did not open')
|
||||
if (errors.length) failures.push(`console errors: ${errors.join('; ')}`)
|
||||
console.log(JSON.stringify({ home, documents, experts, taskModal, profileMenu, errors }, null, 2))
|
||||
console.log(JSON.stringify({ home, documents, experts, taskModal, exploreProjects, profileMenu, errors }, null, 2))
|
||||
if (failures.length) throw new Error(failures.join('\n'))
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Alert, Avatar, Button, Dropdown, Empty, Form, Input, Menu, Modal, Select, Spin, Switch, Tag, Typography } from '@arco-design/web-react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Alert, Avatar, Button, Dropdown, Empty, Form, Input, Menu, Message, Modal, Select, Spin, Switch, Tag, Typography } from '@arco-design/web-react'
|
||||
import {
|
||||
IconApps, IconCheckCircle, IconCheckCircleFill, IconClockCircle, IconDown, IconEmail, IconFile,
|
||||
IconApps, IconCheckCircle, IconCheckCircleFill, IconClockCircle, IconCompass, IconDown, IconFile,
|
||||
IconFolder, IconLeft, IconList, IconMoon, IconPlus, IconRight, IconRobot,
|
||||
IconSun, IconUser,
|
||||
} from '@arco-design/web-react/icon'
|
||||
import {
|
||||
ApiError, createProject, fetchProjectWorkspace, fetchProjects, getCurrentUser,
|
||||
listAIExperts, login, setApiBaseUrl, updateCurrentUser, updateTask,
|
||||
uploadDocument,
|
||||
type ApiSession, type CurrentUser, type Expert, type Project, type Workspace, type WorkspaceTask,
|
||||
} from './api'
|
||||
|
||||
const { Text, Title } = Typography
|
||||
const SESSION_KEY = 'senlin-app-session'
|
||||
type View = 'overview' | 'tasks' | 'inbox' | 'documents' | 'ai' | 'cron'
|
||||
type View = 'overview' | 'tasks' | 'documents' | 'ai' | 'cron'
|
||||
type WorkspaceMode = 'workbench' | 'explore'
|
||||
type SavedSession = ApiSession & { user?: CurrentUser }
|
||||
type TaskWithProject = WorkspaceTask & { projectName: string }
|
||||
|
||||
const navItems: Array<{ id: View; label: string; icon: React.ReactNode }> = [
|
||||
{ id: 'overview', label: '概况', icon: <IconApps /> },
|
||||
{ id: 'tasks', label: '计划', icon: <IconList /> },
|
||||
{ id: 'inbox', label: '收集箱', icon: <IconEmail /> },
|
||||
{ id: 'documents', label: '资料', icon: <IconFile /> },
|
||||
{ id: 'ai', label: 'AI', icon: <IconRobot /> },
|
||||
{ id: 'cron', label: '周期计划', icon: <IconClockCircle /> },
|
||||
{ id: 'cron', label: '计划任务', icon: <IconClockCircle /> },
|
||||
]
|
||||
|
||||
export function App() {
|
||||
@@ -33,6 +34,7 @@ export function App() {
|
||||
const [projects, setProjects] = useState<Project[]>([])
|
||||
const [workspaces, setWorkspaces] = useState<Record<string, Workspace>>({})
|
||||
const [projectId, setProjectId] = useState('')
|
||||
const [workspaceMode, setWorkspaceMode] = useState<WorkspaceMode>('workbench')
|
||||
const [view, setView] = useState<View>('overview')
|
||||
const [dark, setDark] = useState(false)
|
||||
const [loading, setLoading] = useState(Boolean(stored))
|
||||
@@ -90,7 +92,7 @@ export function App() {
|
||||
if (!session) return <Login onLogin={(next) => { setSession(next); setUser(next.user); persistSession(next) }} />
|
||||
|
||||
return (
|
||||
<div className={`app-shell ${dark ? 'theme-dark' : ''}`}>
|
||||
<div className={`app-shell ${dark ? 'theme-dark' : ''} ${!projectId || workspaceMode === 'explore' ? 'no-rail' : ''}`}>
|
||||
<header className="app-header">
|
||||
<div className="app-brand" data-tauri-drag-region><img src="/senlinai-icon.svg" alt="" /><span>森林AI</span></div>
|
||||
<div className="header-actions">
|
||||
@@ -100,21 +102,23 @@ export function App() {
|
||||
</header>
|
||||
|
||||
<section className="project-strip" aria-label="项目列表">
|
||||
<button className={!projectId ? 'project-pill active' : 'project-pill'} onClick={() => setProjectId('')}><IconApps /><span>全部</span></button>
|
||||
<div className="project-scroller">
|
||||
{projects.map((project) => <button key={project.id} className={projectId === project.id ? 'project-pill active' : 'project-pill'} onClick={() => setProjectId(project.id)}><i style={{ background: project.background || '#165DFF' }}>{projectLabel(project)}</i><span>{project.name}</span></button>)}
|
||||
</div>
|
||||
<button className={workspaceMode === 'workbench' ? 'project-pill active' : 'project-pill'} onClick={() => setWorkspaceMode('workbench')}><IconApps /><span>工作台</span></button>
|
||||
<button className={workspaceMode === 'explore' ? 'project-pill active' : 'project-pill'} onClick={() => setWorkspaceMode('explore')}><IconCompass /><span>探索</span></button>
|
||||
<Select className="project-select" value={projectId || 'all'} onChange={(value) => { setProjectId(value === 'all' ? '' : value); setWorkspaceMode('workbench') }} triggerElement={() => <button className="project-pill project-current"><i style={{ background: activeProject?.background || '#165DFF' }}>{activeProject ? projectLabel(activeProject) : <IconFolder />}</i><span>{title}</span><IconDown /></button>}>
|
||||
<Select.Option value="all">全部项目</Select.Option>
|
||||
{projects.map((project) => <Select.Option key={project.id} value={project.id}>{project.name}</Select.Option>)}
|
||||
</Select>
|
||||
<Button type="text" size="mini" shape="circle" aria-label="上一个项目" icon={<IconLeft />} onClick={() => cycleProject(-1)} />
|
||||
<Button type="text" size="mini" shape="circle" aria-label="下一个项目" icon={<IconRight />} onClick={() => cycleProject(1)} />
|
||||
<Button type="primary" size="mini" shape="circle" aria-label="新建项目" icon={<IconPlus />} onClick={() => setProjectModalOpen(true)} />
|
||||
</section>
|
||||
|
||||
{error ? <Alert className="app-alert" type="error" content={error} closable onClose={() => setError('')} /> : null}
|
||||
<main className="app-main"><Spin loading={loading} block>{visibleWorkspaces.length ? <Content view={view} title={title} workspaces={visibleWorkspaces} tasks={tasks} onEditTask={setTaskEditor} onLoadAI={loadAI} experts={experts} /> : <Empty description="还没有项目,点击右上方加号创建一个项目" />}</Spin></main>
|
||||
<main className="app-main"><Spin loading={loading} block>{workspaceMode === 'explore' ? <ExplorePanel projects={projects} onOpen={(id) => { setProjectId(id); setWorkspaceMode('workbench') }} /> : !projectId ? <AllProjectsHome projects={projects} workspaces={visibleWorkspaces} tasks={tasks} onEditTask={setTaskEditor} onCreateProject={() => setProjectModalOpen(true)} onUpload={async (project, file) => { await uploadDocument(session, project, file); await loadWorkspaces() }} /> : visibleWorkspaces.length ? <Content view={view} title={title} workspaces={visibleWorkspaces} tasks={tasks} onEditTask={setTaskEditor} onLoadAI={loadAI} experts={experts} /> : <Empty description="项目加载失败,请刷新后重试" />}</Spin></main>
|
||||
|
||||
<aside className="channel-rail" aria-label="项目子菜单">
|
||||
{projectId && workspaceMode === 'workbench' ? <aside className="channel-rail" aria-label="项目子菜单">
|
||||
{navItems.map((item) => <button key={item.id} title={item.label} className={view === item.id ? 'active' : ''} onClick={() => { setView(item.id); if (item.id === 'ai') void loadAI() }}>{item.icon}<span>{item.label}</span></button>)}
|
||||
</aside>
|
||||
</aside> : null}
|
||||
|
||||
<TaskEditor task={taskEditor} tags={visibleWorkspaces.flatMap((workspace) => workspace.tags.map((tag) => tag.name))} onClose={() => setTaskEditor(null)} onSave={async (draft) => {
|
||||
if (!taskEditor) return
|
||||
@@ -140,7 +144,6 @@ export function App() {
|
||||
|
||||
function Content({ view, title, workspaces, tasks, onEditTask, onLoadAI, experts }: { view: View; title: string; workspaces: Workspace[]; tasks: TaskWithProject[]; onEditTask: (task: TaskWithProject) => void; onLoadAI: () => void; experts: Expert[] }) {
|
||||
if (view === 'tasks' || view === 'overview') return <TaskBoard title={view === 'overview' ? '待办任务' : '工作计划'} subtitle={title} tasks={tasks} onEditTask={onEditTask} />
|
||||
if (view === 'inbox') return <InboxPanel title={title} workspaces={workspaces} />
|
||||
if (view === 'documents') return <DocumentPanel title={title} workspaces={workspaces} />
|
||||
if (view === 'ai') return <AIPanel title={title} workspaces={workspaces} experts={experts} onLoad={onLoadAI} />
|
||||
return <CronPanel title={title} workspaces={workspaces} />
|
||||
@@ -164,9 +167,23 @@ function TaskRow({ task, onClick }: { task: TaskWithProject; onClick: () => void
|
||||
return <button className={`task-row ${task.completed ? 'done' : ''}`} onClick={onClick}><span className="task-check">{task.completed ? <IconCheckCircleFill /> : <IconCheckCircle />}</span><span className="task-copy"><strong>{task.title}</strong><small>{task.summary || '暂无任务说明'}</small></span><span className="task-meta"><Tag color={task.completed ? 'green' : 'arcoblue'}>{task.tag || task.projectName}</Tag><small>{formatDate(task.completedAt || task.createdAt)}</small></span></button>
|
||||
}
|
||||
|
||||
function InboxPanel({ title, workspaces }: { title: string; workspaces: Workspace[] }) {
|
||||
const items = workspaces.flatMap((workspace) => workspace.inbox.map((item) => ({ ...item, projectName: workspace.project.name })))
|
||||
return <Panel title="收集箱" subtitle={title}>{items.map((item) => <article className="detail-row" key={item.id}><IconEmail /><div><strong>{item.title}</strong><p>{item.summary || item.source}</p></div><Tag>{item.projectName}</Tag></article>)}{!items.length && <Empty description="收集箱为空" />}</Panel>
|
||||
|
||||
function AllProjectsHome({ projects, workspaces, tasks, onEditTask, onCreateProject, onUpload }: { projects: Project[]; workspaces: Workspace[]; tasks: TaskWithProject[]; onEditTask: (task: TaskWithProject) => void; onCreateProject: () => void; onUpload: (projectId: string, file: File) => Promise<void> }) {
|
||||
const fileInput = useRef<HTMLInputElement>(null)
|
||||
const [uploadProject, setUploadProject] = useState(projects[0]?.id ?? '')
|
||||
const [uploading, setUploading] = useState(false)
|
||||
useEffect(() => { if (!projects.some((project) => project.id === uploadProject)) setUploadProject(projects[0]?.id ?? '') }, [projects, uploadProject])
|
||||
const pending = tasks.filter((task) => !task.completed)
|
||||
const latestFiles = workspaces.flatMap((workspace) => workspace.documents.map((document) => ({ ...document, projectName: workspace.project.name }))).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).slice(0, 6)
|
||||
const statItems = [
|
||||
{ label: '项目', value: projects.length, color: 'blue' }, { label: '待办', value: pending.length, color: 'orange' },
|
||||
{ label: '文件', value: latestFiles.length, color: 'green' }, { label: '计划任务', value: workspaces.reduce((total, workspace) => total + workspace.cronPlans.length, 0), color: 'purple' },
|
||||
]
|
||||
return <section className="content-page all-projects-home"><div className="content-heading"><div><Text type="secondary">工作台</Text><Title heading={4}>全部项目</Title></div></div><div className="stat-grid">{statItems.map((item) => <div className={`stat-card ${item.color}`} key={item.label}><strong>{item.value}</strong><span>{item.label}</span></div>)}</div><div className="quick-workspace-actions"><Button type="primary" icon={<IconPlus />} onClick={onCreateProject}>创建项目</Button><Button icon={<IconFile />} disabled={!uploadProject} loading={uploading} onClick={() => fileInput.current?.click()}>上传文件</Button><Select value={uploadProject || undefined} placeholder="选择上传项目" onChange={setUploadProject}>{projects.map((project) => <Select.Option key={project.id} value={project.id}>{project.name}</Select.Option>)}</Select><input ref={fileInput} className="hidden-file" type="file" onChange={async (event) => { const file = event.target.files?.[0]; if (!file || !uploadProject) return; setUploading(true); try { await onUpload(uploadProject, file); Message.success('文件已上传') } catch (error) { Message.error(errorMessage(error)) } finally { event.target.value = ''; setUploading(false) } }} /></div><TaskBoard title="待办任务" subtitle="全部项目" tasks={tasks} onEditTask={onEditTask} /><section className="latest-files"><div className="section-label"><strong>最新文件</strong><span>{latestFiles.length}</span></div><div className="detail-list">{latestFiles.map((file) => <article className="detail-row" key={file.id}><IconFile /><div><strong>{file.name}</strong><p>{file.extension || file.mimeType || file.kind} · {formatDate(file.updatedAt)}</p></div><Tag>{file.projectName}</Tag></article>)}{!latestFiles.length && <Empty description="暂无文件,上传一份项目资料开始吧" />}</div></section></section>
|
||||
}
|
||||
|
||||
function ExplorePanel({ projects, onOpen }: { projects: Project[]; onOpen: (id: string) => void }) {
|
||||
return <section className="content-page explore-page"><div className="content-heading"><div><Text type="secondary">探索</Text><Title heading={4}>项目空间</Title></div><Tag color="arcoblue">{projects.length} 个项目</Tag></div><div className="project-card-grid">{projects.map((project) => <button key={project.id} className="project-explore-card" onClick={() => onOpen(project.id)}><i style={{ background: project.background || '#165DFF' }}>{projectLabel(project)}</i><span><strong>{project.name}</strong><small>{project.description || project.identifier}</small></span><IconRight /></button>)}{!projects.length && <Empty description="还没有项目" />}</div></section>
|
||||
}
|
||||
|
||||
function DocumentPanel({ title, workspaces }: { title: string; workspaces: Workspace[] }) {
|
||||
@@ -182,7 +199,7 @@ function AIPanel({ title, workspaces, experts, onLoad }: { title: string; worksp
|
||||
|
||||
function CronPanel({ title, workspaces }: { title: string; workspaces: Workspace[] }) {
|
||||
const plans = workspaces.flatMap((workspace) => workspace.cronPlans.map((item) => ({ ...item, projectName: workspace.project.name })))
|
||||
return <Panel title="周期计划" subtitle={title}>{plans.map((item) => <article className="detail-row" key={item.id}><IconClockCircle /><div><strong>{item.title}</strong><p>{item.schedule} · {item.enabled ? '已启用' : '已停用'}</p></div><Tag>{item.projectName}</Tag></article>)}{!plans.length && <Empty description="暂无周期计划" />}</Panel>
|
||||
return <Panel title="计划任务" subtitle={title}>{plans.map((item) => <article className="detail-row" key={item.id}><IconClockCircle /><div><strong>{item.title}</strong><p>{item.schedule} · {item.enabled ? '已启用' : '已停用'}</p></div><Tag>{item.projectName}</Tag></article>)}{!plans.length && <Empty description="暂无计划任务" />}</Panel>
|
||||
}
|
||||
|
||||
function Panel({ title, subtitle, children }: { title: string; subtitle: string; children: React.ReactNode }) { return <section className="content-page"><div className="content-heading"><div><Text type="secondary">{subtitle}</Text><Title heading={4}>{title}</Title></div></div><div className="detail-list">{children}</div></section> }
|
||||
|
||||
@@ -111,6 +111,14 @@ export function updateTask(session: ApiSession, projectId: string, taskId: strin
|
||||
return apiRequest(`/api/v1/projects/${projectId}/tasks/${taskId}`, { method: 'PATCH', token: session.token, body: input })
|
||||
}
|
||||
|
||||
export function uploadDocument(session: ApiSession, projectId: string, file: File, parentId = '') {
|
||||
useSessionBase(session)
|
||||
const body = new FormData()
|
||||
body.append('file', file)
|
||||
if (parentId) body.append('parentId', parentId)
|
||||
return apiRequest(`/api/v1/projects/${projectId}/documents/uploads`, { method: 'POST', token: session.token, body })
|
||||
}
|
||||
|
||||
export function listAIExperts(session: ApiSession) {
|
||||
useSessionBase(session)
|
||||
return apiRequest<Expert[]>('/api/v1/ai-experts', { token: session.token })
|
||||
|
||||
@@ -20,9 +20,11 @@ button { font: inherit; }
|
||||
.project-pill:hover, .project-pill.active { color: #165dff; background: #e8f3ff; border-color: #d3e7ff; }
|
||||
.project-pill i { display: grid; place-items: center; width: 19px; height: 19px; border-radius: 6px; color: #fff; font-style: normal; font-size: 9px; font-weight: 700; }
|
||||
.project-pill span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; font-weight: 600; }
|
||||
.project-select { min-width: 0; flex: 1; }.project-select .arco-select-view { border: 0; padding: 0; background: transparent; }.project-select .arco-select-view-value { display: none; }.project-current { width: 100%; max-width: none; justify-content: flex-start; }.project-current > svg { margin-left: auto; color: var(--muted); }
|
||||
|
||||
.app-alert { grid-column: 1 / 2; z-index: 2; margin: 8px 12px -46px; align-self: start; }
|
||||
.app-main { grid-column: 1; grid-row: 3; min-width: 0; overflow: auto; padding: 18px 15px 24px; }
|
||||
.app-shell.no-rail .app-main { grid-column: 1 / -1; }
|
||||
.app-main > .arco-spin { min-height: 100%; }
|
||||
.content-page { max-width: 650px; margin: 0 auto; }
|
||||
.content-heading { display: flex; align-items: flex-end; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
|
||||
@@ -63,8 +65,10 @@ button { font: inherit; }
|
||||
.detail-row strong { font-size: 13px; }.detail-row p { margin-top: 3px; color: var(--muted); font-size: 11px; }
|
||||
.expert-chips { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 12px; }
|
||||
.expert-chips span { display: inline-flex; align-items: center; gap: 4px; padding: 4px 7px; border: 1px solid var(--line); border-radius: 999px; background: var(--surface); font-size: 11px; }.expert-chips i { display: grid; place-items: center; width: 17px; height: 17px; border-radius: 50%; font-style: normal; }
|
||||
.stat-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; margin-bottom: 13px; }.stat-card { display: grid; gap: 3px; padding: 11px; border: 1px solid var(--line); border-radius: 11px; background: var(--surface); }.stat-card strong { font-size: 22px; line-height: 1; }.stat-card span { color: var(--muted); font-size: 11px; }.stat-card.blue strong { color: #165dff; }.stat-card.orange strong { color: #ff7d00; }.stat-card.green strong { color: #00b42a; }.stat-card.purple strong { color: #722ed1; }
|
||||
.quick-workspace-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 18px; }.quick-workspace-actions .arco-select { grid-column: 1 / -1; width: 100%; }.hidden-file { display: none; }.all-projects-home .task-page { margin-top: 4px; }.latest-files { padding: 13px; border: 1px solid var(--line); border-radius: 12px; background: var(--surface); }.project-card-grid { display: grid; gap: 9px; }.project-explore-card { display: grid; grid-template-columns: 34px minmax(0, 1fr) auto; align-items: center; gap: 10px; width: 100%; padding: 13px; border: 1px solid var(--line); border-radius: 11px; color: var(--text); background: var(--surface); text-align: left; cursor: pointer; }.project-explore-card:hover { border-color: #9fc9ff; background: #f7faff; }.project-explore-card > i { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 10px; color: #fff; font-style: normal; font-weight: 700; font-size: 11px; }.project-explore-card span { display: grid; min-width: 0; gap: 3px; }.project-explore-card strong, .project-explore-card small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.project-explore-card strong { font-size: 13px; }.project-explore-card small { color: var(--muted); font-size: 11px; }
|
||||
|
||||
.login-page { display: grid; min-height: 100vh; place-items: center; padding: 20px; background: #edf2f9; }.login-card { display: grid; width: min(100%, 360px); gap: 12px; padding: 28px; border: 1px solid #e5e6eb; border-radius: 16px; background: #fff; box-shadow: 0 18px 48px rgba(31, 35, 41, .12); }.login-card > img { width: 34px; }.login-card .arco-typography { margin: 0; }.login-card label { display: grid; gap: 5px; color: #4e5969; font-size: 12px; }
|
||||
.theme-dark .project-strip, .theme-dark .app-header, .theme-dark .task-section, .theme-dark .detail-row, .theme-dark .tag-filter button { color: var(--text); background: var(--surface); }.theme-dark .project-pill { color: var(--muted); }.theme-dark .project-pill.active, .theme-dark .channel-rail button.active { color: #8ac7ff; background: #1d4d77; }.theme-dark .task-row:hover { background: #293847; border-color: #477da7; }.theme-dark .section-label span { color: #8ac7ff; background: #1d4d77; }
|
||||
|
||||
@media (max-width: 360px) { .profile-trigger > span { display: none; }.project-pill { max-width: 90px; }.app-main { padding: 13px 9px 20px; }.task-meta .arco-tag { max-width: 64px; overflow: hidden; }.content-heading h4 { font-size: 18px; } }
|
||||
@media (max-width: 360px) { .profile-trigger > span { display: none; }.project-pill { max-width: 90px; }.project-strip { gap: 2px; padding-inline: 5px; }.app-main { padding: 13px 9px 20px; }.task-meta .arco-tag { max-width: 64px; overflow: hidden; }.content-heading h4 { font-size: 18px; }.stat-grid { grid-template-columns: repeat(2, 1fr); } }
|
||||
|
||||
Reference in New Issue
Block a user