feat: add project channel workbench shell

This commit is contained in:
2026-07-19 19:12:28 +08:00
parent 6c12d4b0db
commit 95dce81161
6 changed files with 163 additions and 4 deletions

View File

@@ -1,4 +1,5 @@
<script lang="ts">
import ProjectWorkbench from '../features/workbench/ProjectWorkbench.svelte';
import ServerLogin from '../features/auth/ServerLogin.svelte';
let apiBase = localStorage.getItem('apiBase') ?? 'http://localhost:8080';
@@ -12,10 +13,7 @@
</script>
{#if currentUser}
<main class="workbench-placeholder" aria-label="Project workbench">
<h1>SenlinAI Workbench</h1>
<p>Signed in as {currentUser.account}</p>
</main>
<ProjectWorkbench {currentUser} />
{:else}
<ServerLogin {apiBase} onLogin={handleLogin} />
{/if}

View File

@@ -0,0 +1,52 @@
<script lang="ts">
import type { AISessionItem, WorkbenchChannel, WorkbenchProject } from './types';
export let project: WorkbenchProject;
export let channels: WorkbenchChannel[];
export let selectedChannelID: string;
export let tags: string[];
export let recentSessions: AISessionItem[];
export let onSelectChannel: (channelID: string) => void;
</script>
<aside class="channel-sidebar" aria-label="Project channels">
<header>
<div>
<p>Current project</p>
<h2>{project.name}</h2>
</div>
<button aria-label="Project settings">&#9881;</button>
</header>
<section class="tag-row" aria-label="Project tags">
{#each tags as tag}
<button type="button">#{tag}</button>
{/each}
</section>
<section class="channel-group">
{#each channels as channel}
<button
class:active={channel.id === selectedChannelID}
aria-pressed={channel.id === selectedChannelID}
aria-label={`${channel.title}${channel.count ? ` ${channel.count}` : ''}`}
on:click={() => onSelectChannel(channel.id)}
>
<span>{channel.title}</span>
{#if channel.count !== undefined}
<small>{channel.count}</small>
{/if}
</button>
{/each}
</section>
<section class="recent-sessions" aria-label="Recent sessions">
<h3>Recent sessions</h3>
{#each recentSessions as session}
<button type="button">
<span>{session.title}</span>
<small>{session.updatedAt}</small>
</button>
{/each}
</section>
</aside>

View File

@@ -0,0 +1,26 @@
<script lang="ts">
import type { WorkbenchProject } from './types';
export let projects: WorkbenchProject[];
export let selectedProjectID: number;
export let onSelectProject: (projectID: number) => void;
</script>
<nav class="project-rail" aria-label="Project list">
<button class="rail-logo" aria-label="Dashboard">SA</button>
{#each projects as project}
<button
class:active={project.id === selectedProjectID}
aria-pressed={project.id === selectedProjectID}
aria-label={project.name}
title={project.name}
on:click={() => onSelectProject(project.id)}
>
<span>{project.initials}</span>
{#if project.unreadCount}
<small>{project.unreadCount}</small>
{/if}
</button>
{/each}
<button class="add-project" aria-label="Create project">+</button>
</nav>

View File

@@ -0,0 +1,41 @@
<script lang="ts">
import { getProjectWorkspace, workbenchProjects } from './mockData';
import ProjectRail from './ProjectRail.svelte';
import ProjectChannelSidebar from './ProjectChannelSidebar.svelte';
import WorkspaceTopbar from './WorkspaceTopbar.svelte';
export let currentUser: { account: string };
let selectedProjectID = workbenchProjects[0].id;
$: workspace = getProjectWorkspace(selectedProjectID);
$: selectedChannelID = workspace.channels[0].id;
function selectProject(projectID: number) {
selectedProjectID = projectID;
}
function selectChannel(channelID: string) {
selectedChannelID = channelID;
}
</script>
<main class="workbench-shell">
<WorkspaceTopbar account={currentUser.account} />
<div class="workbench-body">
<ProjectRail projects={workbenchProjects} {selectedProjectID} onSelectProject={selectProject} />
<ProjectChannelSidebar
project={workspace.project}
channels={workspace.channels}
{selectedChannelID}
tags={workspace.tags}
recentSessions={workspace.recentSessions}
onSelectChannel={selectChannel}
/>
<section class="channel-stage" aria-label="Channel content">
<h1>{workspace.channels.find((channel) => channel.id === selectedChannelID)?.title}</h1>
</section>
<aside class="object-inspector" aria-label="Object inspector">
<p>Select an item to inspect discussion, properties, and actions.</p>
</aside>
</div>
</main>

View File

@@ -0,0 +1,26 @@
import '@testing-library/jest-dom/vitest';
import { fireEvent, render, screen } from '@testing-library/svelte';
import { describe, expect, it } from 'vitest';
import ProjectWorkbench from './ProjectWorkbench.svelte';
describe('ProjectWorkbench', () => {
it('renders project rail, channel sidebar, topbar, and inspector', () => {
render(ProjectWorkbench, { props: { currentUser: { account: 'david@example.com' } } });
expect(screen.getByLabelText('Project list')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Project A1' })).toHaveAttribute('aria-pressed', 'true');
expect(screen.getByLabelText('Project channels')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Message Flow 36' })).toBeInTheDocument();
expect(screen.getByLabelText('Global search')).toBeInTheDocument();
expect(screen.getByLabelText('Object inspector')).toBeInTheDocument();
});
it('refreshes channels when switching projects', async () => {
render(ProjectWorkbench, { props: { currentUser: { account: 'david@example.com' } } });
await fireEvent.click(screen.getByRole('button', { name: 'Project A2' }));
expect(screen.getByRole('button', { name: 'Project A2' })).toHaveAttribute('aria-pressed', 'true');
expect(screen.getByText('Ops Dashboard')).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,16 @@
<script lang="ts">
export let account: string;
</script>
<header class="workspace-topbar">
<div class="brand-mark">SenlinAI</div>
<label class="search-box">
<span>Search</span>
<input aria-label="Global search" placeholder="Search projects, tasks, notes, AI sessions" />
</label>
<div class="topbar-actions">
<button aria-label="Back">&#8592;</button>
<button aria-label="Forward">&#8594;</button>
<button aria-label={`Account ${account}`}>{account.slice(0, 2).toUpperCase()}</button>
</div>
</header>