Files
agent/.superpowers/sdd/task-1-review-package.md
2026-07-20 08:02:20 +08:00

8.2 KiB

Review package Task 1

Commits

5b95668 feat: add full workbench login page

Stat

apps/web/src/app/App.svelte | 79 ++++---------------------- apps/web/src/features/auth/ServerLogin.svelte | 63 +++++++++++++++----- apps/web/src/features/auth/ServerLogin.test.ts | 49 ++++++++++++++++ 3 files changed, 110 insertions(+), 81 deletions(-)

Diff

diff --git a/apps/web/src/app/App.svelte b/apps/web/src/app/App.svelte index a4c6389..a5d8dce 100644 --- a/apps/web/src/app/App.svelte +++ b/apps/web/src/app/App.svelte @@ -1,76 +1,21 @@

<script lang="ts"> - import { onMount } from 'svelte'; - import ProjectDashboard from '../features/projects/ProjectDashboard.svelte'; import ServerLogin from '../features/auth/ServerLogin.svelte'; - import SuggestionList from '../features/inbox/SuggestionList.svelte'; - import type { Suggestion } from '../features/inbox/types'; - import { createApi, type ProjectDashboardSummary } from '../lib/api'; - - const emptyDashboard: ProjectDashboardSummary = { - project_id: 1, - pending_inbox_count: 0, - open_task_count: 0, - recent_note_count: 0, - recent_session_count: 0, - }; let apiBase = localStorage.getItem('apiBase') ?? 'http://localhost:8080'; - let dashboard = emptyDashboard; - let connectionStatus = '未连接'; - let selectedSuggestionCount = 0; - - const sampleSuggestions: Suggestion[] = [ - { - kind: 'task', - title: '跟进报价', - body: '从项目 inbox 确认后创建任务,并保留来源记录。', - }, - { - kind: 'note', - title: '客户背景', - body: '把对话中的背景信息沉淀成项目笔记。', - }, - ]; + let currentUser: { account: string } | null = null; - onMount(() => { - void loadDashboard(); - }); - - function handleServerChange(event: CustomEvent<{ apiBase: string }>) { - apiBase = event.detail.apiBase; + function handleLogin(detail: { apiBase: string; account: string }) { + apiBase = detail.apiBase; localStorage.setItem('apiBase', apiBase); - void loadDashboard(); - } - - async function loadDashboard() { - connectionStatus = '连接中'; - try { - dashboard = await createApi(apiBase).getProjectDashboard(1); - connectionStatus = '已连接'; - } catch { - dashboard = emptyDashboard; - connectionStatus = '无法连接服务器'; - } - } - - function handleConfirm(selected: Suggestion[]) { - selectedSuggestionCount = selected.length; + currentUser = { account: detail.account }; } </script>

-

  • 项目工作台

  • <ServerLogin {apiBase} on:serverChange={handleServerChange} />
  • {connectionStatus}

  •  <h2>AI 整理建议</h2>
    
  •  <SuggestionList suggestions={sampleSuggestions} onConfirm={handleConfirm} />
    
  •  <p class="selection-status" aria-live="polite">已选择 {selectedSuggestionCount} 项</p>
    

-

+{#if currentUser}

  • SenlinAI Workbench

  • Signed in as {currentUser.account}

+{:else}

  • <ServerLogin {apiBase} onLogin={handleLogin} /> +{/if} diff --git a/apps/web/src/features/auth/ServerLogin.svelte b/apps/web/src/features/auth/ServerLogin.svelte index 8e80261..8e16cb9 100644 --- a/apps/web/src/features/auth/ServerLogin.svelte +++ b/apps/web/src/features/auth/ServerLogin.svelte @@ -1,28 +1,63 @@
<script lang="ts"> - import { createEventDispatcher } from 'svelte'; - export let apiBase: string; + export let onLogin: (detail: { apiBase: string; account: string }) => void = () => {}; - const dispatch = createEventDispatcher<{ serverChange: { apiBase: string } }>(); let server = apiBase; - - function saveServer() { - const normalized = normalizeServer(server); - server = normalized; - dispatch('serverChange', { apiBase: normalized }); - } + let account = ''; + let password = ''; + let error = ''; function normalizeServer(value: string) { const trimmed = value.trim(); if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) { return trimmed; } return `http://${trimmed}`; } + + function submitLogin() { + error = ''; + if (!account.trim() || !password.trim()) { + error = 'Enter an account and password.'; + return; + } + + const normalized = normalizeServer(server); + server = normalized; + onLogin({ apiBase: normalized, account: account.trim() }); + } </script>

-<form aria-label="服务器登录" class="server-login" on:submit|preventDefault={saveServer}>

  • 保存服务器 - +
  •  <p>Private workbench</p>
    
  •  <h1 id="login-title">SenlinAI Workbench</h1>
    
  •  <label for="server-address">Server IP or domain</label>
    
  •  <input id="server-address" name="server" bind:value={server} placeholder="http://localhost:8080" />
    
  •  <label for="login-account">Email or username</label>
    
  •  <input id="login-account" name="account" bind:value={account} autocomplete="username" />
    
  •  <label for="login-password">Password</label>
    
  •  <input
    
  •    id="login-password"
    
  •    name="password"
    
  •    type="password"
    
  •    bind:value={password}
    
  •    autocomplete="current-password"
    
  •  />
    
  •  {#if error}
    
  •    <p class="form-error" aria-live="polite">{error}</p>
    
  •  {/if}
    
  •  <button type="submit">Log in</button>
    
  •  <p class="login-note">The server address is remembered on this device.</p>
    

+ diff --git a/apps/web/src/features/auth/ServerLogin.test.ts b/apps/web/src/features/auth/ServerLogin.test.ts new file mode 100644 index 0000000..7298b4b --- /dev/null +++ b/apps/web/src/features/auth/ServerLogin.test.ts @@ -0,0 +1,49 @@ +import '@testing-library/jest-dom/vitest'; +import { fireEvent, render, screen } from '@testing-library/svelte'; +import { describe, expect, it, vi } from 'vitest'; +import ServerLogin from './ServerLogin.svelte'; + +describe('ServerLogin', () => {

  • it('renders server, account, and password fields', () => {
  • render(ServerLogin, { props: { apiBase: 'http://localhost:8080' } });
  • expect(screen.getByLabelText('Server IP or domain')).toBeInTheDocument();
  • expect(screen.getByLabelText('Email or username')).toBeInTheDocument();
  • expect(screen.getByLabelText('Password')).toBeInTheDocument();
  • expect(screen.getByRole('button', { name: 'Log in' })).toBeInTheDocument();
  • });
  • it('normalizes server address and emits login details', async () => {
  • const onLogin = vi.fn();
  • render(ServerLogin, {
  •  props: {
    
  •    apiBase: 'localhost:8080',
    
  •    onLogin,
    
  •  },
    
  • });
  • await fireEvent.input(screen.getByLabelText('Server IP or domain'), {
  •  target: { value: '10.0.0.12:8080' },
    
  • });
  • await fireEvent.input(screen.getByLabelText('Email or username'), {
  •  target: { value: 'david@example.com' },
    
  • });
  • await fireEvent.input(screen.getByLabelText('Password'), {
  •  target: { value: 'secret' },
    
  • });
  • await fireEvent.click(screen.getByRole('button', { name: 'Log in' }));
  • expect(onLogin).toHaveBeenCalledWith({
  •  apiBase: 'http://10.0.0.12:8080',
    
  •  account: 'david@example.com',
    
  • });
  • });
  • it('shows an error when account or password is missing', async () => {
  • render(ServerLogin, { props: { apiBase: 'http://localhost:8080' } });
  • await fireEvent.click(screen.getByRole('button', { name: 'Log in' }));
  • expect(screen.getByText('Enter an account and password.')).toBeInTheDocument();
  • }); +});