Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions changelog/unreleased/access-chat-chrome.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
### English

- The Access chat empty state shows an icon, each assistant reply carries the provider mark, and copy uses the same button as the rest of the console.
- The login page plays the live in-flight demo on the left instead of the static title, at a slower pace than the overview card.

### 中文

- 访问页聊天的空状态带上图标,助手回复显示供应商标,复制按钮与控制台其他位置一致。
- 登录页左侧改成实时进行中的演示动画,节奏比总览页慢,不再是静态标题。
24 changes: 16 additions & 8 deletions frontend/src/components/access/ChatMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useEffect, useRef, useState } from 'react'
import { PageAlert } from '@/components/ui/PageAlert'
import { Skeleton } from '@heroui/react'
import { CopySimple, FileText } from '@phosphor-icons/react'
import { Button, Skeleton } from '@heroui/react'
import { FileText } from '@phosphor-icons/react'
import { CopyIcon } from '@/components/motion/CopyIcon'
import { ProviderMark } from '@/components/ProviderMark'
import { useI18n } from '@/hooks/useI18n'
import { copyText } from '@/lib/clipboard'
import { formatCompact, formatCredits } from '@/lib/format'
Expand Down Expand Up @@ -142,6 +144,11 @@ export function ChatMessageView({ message }: { message: ThreadMessage }) {

return (
<div className="group/access-msg max-w-[96%] space-y-1.5 sm:max-w-[88%]">
<div className="flex items-center gap-1.5 px-0.5">
<span className="grid size-5 shrink-0 place-items-center rounded-lg border border-border bg-surface-secondary">
<ProviderMark provider={message.provider} size={13} />
</span>
</div>
{message.reasoning || streaming ? (
<ThinkingBlock
text={message.reasoning || ''}
Expand All @@ -168,14 +175,15 @@ export function ChatMessageView({ message }: { message: ThreadMessage }) {
<div className="access-msg-foot space-y-1 px-0.5 pt-0.5">
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
{canCopy ? (
<button
type="button"
className="inline-flex h-5 items-center gap-1 text-caption text-muted/80 opacity-70 transition-opacity hover:text-foreground hover:opacity-100"
onClick={() => void onCopy()}
<Button
size="sm"
variant="ghost"
className="h-5 min-h-5 gap-1 px-1.5 text-caption text-muted/80"
onPress={() => void onCopy()}
>
<CopySimple size={12} />
<CopyIcon copied={copied} size={12} />
{copied ? t('copied') : t('copy')}
</button>
</Button>
) : null}
<UsageFoot message={message} />
</div>
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/access/ChatThread.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useRef } from 'react'
import { ChatCircleDots } from '@phosphor-icons/react'
import { EmptyPanel } from '@/components/ui/EmptyPanel'
import { useI18n } from '@/hooks/useI18n'
import { ChatMessageView } from './ChatMessage'
Expand Down Expand Up @@ -33,6 +34,7 @@ export function ChatThread({
{empty ? (
<EmptyPanel
className="min-h-56"
icon={<ChatCircleDots size={22} />}
title={t('chatTryEmptyTitle')}
hint={t('chatTryEmptyHint')}
action={
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/access/demoSeed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export function buildDemoThread(): ThreadMessage[] {
{
id: 'demo-assistant-1',
role: 'assistant',
provider: 'workbuddy',
content:
'The screenshot shows the Access playground. I looked up the chat completions route and drafted a short summary below.',
reasoning:
Expand Down Expand Up @@ -73,6 +74,7 @@ export function buildDemoThinking(): ThreadMessage[] {
{
id: 'demo-assistant-thinking',
role: 'assistant',
provider: 'workbuddy',
content: '',
reasoning:
'Scan the stage chrome, then the composer controls, then draft a short reply without inventing product claims.',
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/access/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export type ThreadMessageUsage = {
export type ThreadMessage = {
id: string
role: 'user' | 'assistant'
/** Account provider that produced an assistant turn. Drives the existing brand mark. */
provider?: string
content: string
attachments?: ChatAttachment[]
reasoning?: string
Expand Down
131 changes: 80 additions & 51 deletions frontend/src/components/overview/InflightLiveCard.tsx

Large diffs are not rendered by default.

82 changes: 42 additions & 40 deletions frontend/src/lib/mockInflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,63 +129,64 @@ export const MOCK_INFLIGHT: MockInflightSnapshot = {
totals: { done: 186, failed: 4 },
workers: [
{
id: 'acc-wb-a1', name: 'wb-a1', provider: 'workbuddy', busy: true, progress: 0.72, jobId: 'j1', concurrency: 4, inFlight: 3,
id: 'acc-wb-a1', name: 'WorkBuddy', provider: 'workbuddy', busy: true, progress: 0.72, jobId: 'j1', concurrency: 4, inFlight: 3,
cells: [
{ id: 'j1', name: 'chat · sonnet', startedAtMs: 0, elapsedMs: 11_200 },
{ id: 'j1b', name: 'chat · opus', startedAtMs: 0, elapsedMs: 8400 },
{ id: 'j1c', name: 'embed · idx', startedAtMs: 0, elapsedMs: 4100 },
{ id: 'j1', name: 'glm-5.2', startedAtMs: 0, elapsedMs: 11_200 },
{ id: 'j1b', name: 'kimi-k2.7-code', startedAtMs: 0, elapsedMs: 8400 },
{ id: 'j1c', name: 'qwen3-embedding', startedAtMs: 0, elapsedMs: 4100 },
],
},
{
id: 'acc-wb-a2', name: 'wb-a2', provider: 'workbuddy', busy: true, progress: 0.41, jobId: 'j2', rateLimited: true, concurrency: 2, inFlight: 1,
cells: [{ id: 'j2', name: 'embed · batch', startedAtMs: 0, elapsedMs: 9400 }],
id: 'acc-wb-a2', name: 'Zhipu', provider: 'zhipu', busy: true, progress: 0.41, jobId: 'j2', rateLimited: true, concurrency: 2, inFlight: 1,
cells: [{ id: 'j2', name: 'deepseek-v4-pro', startedAtMs: 0, elapsedMs: 9400 }],
},
{
id: 'acc-qd-b1', name: 'qd-b1', provider: 'qoder', busy: true, progress: 0.88, jobId: 'j3', concurrency: 1, inFlight: 1,
cells: [{ id: 'j3', name: 'chat · gpt', startedAtMs: 0, elapsedMs: 6200 }],
id: 'acc-qd-b1', name: 'Qoder', provider: 'qoder', busy: true, progress: 0.88, jobId: 'j3', concurrency: 1, inFlight: 1,
cells: [{ id: 'j3', name: 'glm-5.3', startedAtMs: 0, elapsedMs: 6200 }],
},
{ id: 'acc-qd-b2', name: 'qd-b2', provider: 'qoder', busy: false, progress: 0, concurrency: 4, inFlight: 0, cells: [] },
{ id: 'acc-qd-b2', name: 'Kimi', provider: 'kimi', busy: false, progress: 0, concurrency: 4, inFlight: 0, cells: [] },
{
id: 'acc-trae-c1', name: 'trae-c1', provider: 'trae', busy: true, progress: 0.23, jobId: 'j4', concurrency: 3, inFlight: 2,
id: 'acc-trae-c1', name: 'Trae', provider: 'trae', busy: true, progress: 0.23, jobId: 'j4', concurrency: 3, inFlight: 2,
cells: [
{ id: 'j4', name: 'tools · patch', startedAtMs: 0, elapsedMs: 3100 },
{ id: 'j4b', name: 'chat · flash', startedAtMs: 0, elapsedMs: 1800 },
{ id: 'j4', name: 'qwen3-coder', startedAtMs: 0, elapsedMs: 3100 },
{ id: 'j4b', name: 'glm-5.3-flash', startedAtMs: 0, elapsedMs: 1800 },
],
},
{ id: 'acc-codex-d1', name: 'codex-d1', provider: 'codex', busy: false, progress: 0, rateLimited: true, concurrency: 2, inFlight: 0, cells: [] },
{ id: 'acc-codex-d1', name: 'Codex', provider: 'codex', busy: false, progress: 0, rateLimited: true, concurrency: 2, inFlight: 0, cells: [] },
],
jobs: [
{ id: 'j1', name: 'chat · sonnet', kind: 'chat', status: 'running', accountId: 'acc-wb-a1', progress: 0.72, elapsedMs: 11_200, sticky: true },
{ id: 'j1b', name: 'chat · opus', kind: 'chat', status: 'running', accountId: 'acc-wb-a1', progress: 0.55, elapsedMs: 8400 },
{ id: 'j1c', name: 'embed · idx', kind: 'embed', status: 'running', accountId: 'acc-wb-a1', progress: 0.3, elapsedMs: 4100 },
{ id: 'j2', name: 'embed · batch', kind: 'embed', status: 'running', accountId: 'acc-wb-a2', progress: 0.41, elapsedMs: 9400 },
{ id: 'j3', name: 'chat · gpt', kind: 'chat', status: 'running', accountId: 'acc-qd-b1', progress: 0.88, elapsedMs: 6200 },
{ id: 'j4', name: 'tools · patch', kind: 'tools', status: 'running', accountId: 'acc-trae-c1', progress: 0.23, elapsedMs: 3100 },
{ id: 'j4b', name: 'chat · flash', kind: 'chat', status: 'running', accountId: 'acc-trae-c1', progress: 0.15, elapsedMs: 1800 },
{ id: 'j5', name: 'chat · wait', kind: 'chat', status: 'queued', progress: 0, elapsedMs: 7200 },
{ id: 'j6', name: 'retry · cool', kind: 'chat', status: 'queued', progress: 0, elapsedMs: 5100 },
{ id: 'j7', name: 'chat · sticky', kind: 'chat', status: 'queued', progress: 0, elapsedMs: 2800, sticky: true, priority: 'high' },
{ id: 'j8', name: 'embed · docs', kind: 'embed', status: 'queued', progress: 0, elapsedMs: 1400 },
{ id: 'j9', name: 'tools · apply', kind: 'tools', status: 'queued', progress: 0, elapsedMs: 900 },
{ id: 'j10', name: 'chat · flash', kind: 'chat', status: 'queued', progress: 0, elapsedMs: 600 },
{ id: 'j11', name: 'embed · idx', kind: 'embed', status: 'queued', progress: 0, elapsedMs: 400 },
{ id: 'j12', name: 'chat · opus', kind: 'chat', status: 'queued', progress: 0, elapsedMs: 200, priority: 'high' },
{ id: 'j1', name: 'glm-5.2', kind: 'chat', status: 'running', accountId: 'acc-wb-a1', progress: 0.72, elapsedMs: 11_200, sticky: true },
{ id: 'j1b', name: 'kimi-k2.7-code', kind: 'chat', status: 'running', accountId: 'acc-wb-a1', progress: 0.55, elapsedMs: 8400 },
{ id: 'j1c', name: 'qwen3-embedding', kind: 'embed', status: 'running', accountId: 'acc-wb-a1', progress: 0.3, elapsedMs: 4100 },
{ id: 'j2', name: 'deepseek-v4-pro', kind: 'embed', status: 'running', accountId: 'acc-wb-a2', progress: 0.41, elapsedMs: 9400 },
{ id: 'j3', name: 'glm-5.3', kind: 'chat', status: 'running', accountId: 'acc-qd-b1', progress: 0.88, elapsedMs: 6200 },
{ id: 'j4', name: 'qwen3-coder', kind: 'tools', status: 'running', accountId: 'acc-trae-c1', progress: 0.23, elapsedMs: 3100 },
{ id: 'j4b', name: 'glm-5.3-flash', kind: 'chat', status: 'running', accountId: 'acc-trae-c1', progress: 0.15, elapsedMs: 1800 },
{ id: 'j5', name: 'deepseek-v4.1-flash', kind: 'chat', status: 'queued', progress: 0, elapsedMs: 7200 },
{ id: 'j6', name: 'glm-5.2', kind: 'chat', status: 'queued', progress: 0, elapsedMs: 5100 },
{ id: 'j7', name: 'kimi-k2.7-code', kind: 'chat', status: 'queued', progress: 0, elapsedMs: 2800, sticky: true, priority: 'high' },
{ id: 'j8', name: 'qwen3-embedding', kind: 'embed', status: 'queued', progress: 0, elapsedMs: 1400 },
{ id: 'j9', name: 'qwen3-coder', kind: 'tools', status: 'queued', progress: 0, elapsedMs: 900 },
{ id: 'j10', name: 'glm-5.3-flash', kind: 'chat', status: 'queued', progress: 0, elapsedMs: 600 },
{ id: 'j11', name: 'deepseek-v4-pro', kind: 'embed', status: 'queued', progress: 0, elapsedMs: 400 },
{ id: 'j12', name: 'glm-5.3', kind: 'chat', status: 'queued', progress: 0, elapsedMs: 200, priority: 'high' },
],
rateLimits: [
{ id: 'rl1', label: 'wb-a2 限流', remainingMs: 97_000, from: 0.55, until: 0.72 },
{ id: 'rl2', label: 'codex-d1 冷却', remainingMs: 41_000, from: 0.82, until: 1 },
{ id: 'rl1', label: 'Zhipu 限流', remainingMs: 97_000, from: 0.55, until: 0.72 },
{ id: 'rl2', label: 'Codex 冷却', remainingMs: 41_000, from: 0.82, until: 1 },
],
// Soft hump under rate-limit, then partial drain.
depthSeries: [1, 1, 2, 2, 3, 4, 5, 6, 7, 6, 5, 4, 3, 4, 5, 6, 5, 4, 3, 3, 2, 3, 4, 3],
}

const SEED_NAMES: Array<{ name: string; kind: MockJobKind; sticky?: boolean }> = [
{ name: 'chat · opus', kind: 'chat' },
{ name: 'embed · idx', kind: 'embed' },
{ name: 'tools · apply', kind: 'tools' },
{ name: 'chat · flash', kind: 'chat', sticky: true },
{ name: 'chat · wait', kind: 'chat' },
{ name: 'embed · docs', kind: 'embed' },
{ name: 'glm-5.3', kind: 'chat' },
{ name: 'qwen3-embedding', kind: 'embed' },
{ name: 'qwen3-coder', kind: 'tools' },
{ name: 'glm-5.3-flash', kind: 'chat', sticky: true },
{ name: 'deepseek-v4.1-flash', kind: 'chat' },
{ name: 'deepseek-v4-pro', kind: 'embed' },
{ name: 'kimi-k2.7-code', kind: 'chat' },
]

let seq = 100
Expand Down Expand Up @@ -253,8 +254,8 @@ export function tickMockInflight(prev: MockInflightSnapshot, dtMs: number): Mock
.filter((r) => r.remainingMs > 0)
// Keep rateLimited flag in sync with remaining windows (seed label → slot).
for (const w of next.workers) {
if (w.id === 'acc-wb-a2') w.rateLimited = next.rateLimits.some((r) => r.label.includes('wb-a2'))
else if (w.id === 'acc-codex-d1') w.rateLimited = next.rateLimits.some((r) => r.label.includes('codex-d1'))
if (w.id === 'acc-wb-a2') w.rateLimited = next.rateLimits.some((r) => r.label.includes('Zhipu'))
else if (w.id === 'acc-codex-d1') w.rateLimited = next.rateLimits.some((r) => r.label.includes('Codex'))
}

const jobById = new Map(next.jobs.map((j) => [j.id, j]))
Expand Down Expand Up @@ -371,9 +372,10 @@ export function tickMockInflight(prev: MockInflightSnapshot, dtMs: number): Mock
}
}

// Soft enqueue so the waiting rail doesn’t drain dry.
// Soft enqueue so the waiting rail doesn’t drain dry. Keep enough
// queued jobs that the rail shows several timed pills, not just dots.
const waiting = next.jobs.filter((j) => j.status === 'queued').length
if (waiting < 6 && next.generatedAt % 2800 < dt) {
if (waiting < 10 && next.generatedAt % 1600 < dt) {
const pick = SEED_NAMES[seq % SEED_NAMES.length]
seq += 1
// Every ~4th job is high priority (lands at back, then glides past).
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/pages/AccessPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,9 @@ export function AccessPage() {
const assistantMessage: ThreadMessage = {
id: assistantId,
role: 'assistant',
provider: accounts.find((account) => account.id === selectedAccount)?.provider
|| models.find((item) => item.id === selectedModel)?.provider
|| '',
content: '',
status: 'streaming',
}
Expand Down
23 changes: 8 additions & 15 deletions frontend/src/pages/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Navigate, useLocation, useNavigate } from 'react-router-dom'
import { Button, Card, InputGroup, Label, TextField, Toolbar, Tooltip } from '@heroui/react'
import { ArrowRight, Eye, EyeSlash, Globe, Moon, Sun } from '@phosphor-icons/react'
import { BrandMark } from '@/components/BrandMark'
import { InflightLiveCard } from '@/components/overview/InflightLiveCard'
import { PageAlert } from '@/components/ui/PageAlert'
import { useI18n } from '@/hooks/useI18n'
import { useApiKey } from '@/hooks/useApiKey'
Expand Down Expand Up @@ -51,8 +52,8 @@ export function LoginPage() {
return (
<div className="relative min-h-dvh overflow-hidden bg-background text-foreground">
<div className="absolute inset-x-0 top-0 h-px bg-border" />
<main ref={pageRef} className="relative z-10 mx-auto grid min-h-dvh w-full max-w-[1480px] lg:grid-cols-[minmax(0,1fr)_minmax(480px,45%)]">
<section className="flex min-h-[52vh] flex-col border-separator px-5 py-6 sm:px-10 sm:py-8 lg:min-h-dvh lg:border-r lg:px-14 lg:py-10 xl:px-20">
<main ref={pageRef} className="relative z-10 mx-auto grid min-h-dvh w-full max-w-[1480px] lg:grid-cols-[minmax(0,1fr)_minmax(380px,30rem)]">
<section className="flex min-h-[52vh] flex-col px-5 py-6 sm:px-8 sm:py-8 lg:min-h-dvh lg:px-8 lg:py-10 xl:px-10">
<header className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3">
<BrandMark size={36} />
Expand Down Expand Up @@ -81,16 +82,8 @@ export function LoginPage() {
</Toolbar>
</header>

<div className="my-auto max-w-3xl py-16 lg:py-24">
<p data-gsap-reveal className="mb-5 mono text-caption text-muted">:3010</p>
<h1 data-gsap-reveal className="max-w-3xl text-[clamp(2.25rem,4vw,3.6rem)] font-semibold leading-[0.98] tracking-[-0.045em] text-balance">
{t('loginTitle')}
</h1>
<p data-gsap-reveal className="mt-6 max-w-xl text-section font-normal text-muted sm:text-title sm:font-normal">{t('loginLead')}</p>

<p data-gsap-reveal className="mt-10 max-w-xl pt-5 text-body leading-6 text-muted">
{t('loginHint')}
</p>
<div className="my-auto w-full py-8 lg:py-10">
<InflightLiveCard t={t} defaultExpanded forceMockDemo bare live={null} mockPace={0.45} />
</div>

<footer className="hidden items-center justify-between text-caption text-muted lg:flex">
Expand All @@ -99,10 +92,10 @@ export function LoginPage() {
</footer>
</section>

<section className="flex items-center px-5 py-10 sm:px-10 lg:px-12 xl:px-16">
<Card data-gsap-reveal className="w-full p-0">
<section className="flex items-center px-5 py-10 sm:px-8 lg:px-8 xl:px-10">
<Card data-gsap-reveal variant="transparent" className="w-full bg-transparent p-0 shadow-none">
<form onSubmit={(event) => void onSubmit(event)}>
<Card.Header className="border-b border-separator px-6 py-6 sm:px-8">
<Card.Header className="px-2 py-6 sm:px-4">
<div className="flex min-w-0 items-center gap-3">
<BrandMark size={28} loading={busy} />
<div>
Expand Down
Loading
Loading