Skip to content
Open
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
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ RUN pnpm install --frozen-lockfile
# + UI Vite build + services/uta tsup) then `tsup` bundles the Alice
# backend into `dist/main.js`. UTA service ends up at
# `services/uta/dist/uta.js`.
# Note: --filter excludes @traderalice/desktop (Electron app) which is not
# needed for the server image and requires native dependencies.
COPY . .
RUN pnpm build
RUN pnpm prebuild && pnpm turbo run build --filter='!@traderalice/desktop' && pnpm exec tsup src/main.ts --format esm --dts

# Strip dev deps before the runtime stage harvests node_modules. With
# `electron` + `electron-builder` (each ~500MB) in devDependencies, this
Expand Down
58 changes: 58 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@
"@xyflow/react": "^12.10.2",
"dompurify": "^3.4.0",
"highlight.js": "^11.11.1",
"i18next": "^26.2.0",
"lightweight-charts": "^5.1.0",
"lucide-react": "^1.14.0",
"marked": "^15.0.12",
"marked-highlight": "^2.2.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-i18next": "^17.0.8",
"react-resizable-panels": "^4.11.0",
"react-router-dom": "^7.13.1",
"recharts": "^3.8.0",
Expand Down
4 changes: 3 additions & 1 deletion ui/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Group, Panel, Separator, useDefaultLayout } from 'react-resizable-panels'
import { ActivityBar } from './components/ActivityBar'
import { Sidebar } from './components/Sidebar'
Expand All @@ -11,14 +12,15 @@ import { findSectionForActivity } from './sections'
import { UrlAdopter } from './tabs/UrlAdopter'
import { useWorkspace } from './tabs/store'
import { getFocusedTab } from './tabs/types'
import './i18n'

/**
* Activity-bar pages — only items that appear as icons in the ActivityBar.
* Each maps to one or more tab kinds via tabs/registry.ts (defaultSpecForActivity).
*/
export type Page =
| 'chat' | 'inbox' | 'workspaces' | 'portfolio' | 'news' | 'automation' | 'market'
| 'trading-as-git'
| 'trading-as-git' | 'trading-accounts'
| 'settings' | 'dev'
| 'traditional-chat' | 'notifications-legacy' | 'connectors-legacy'

Expand Down
4 changes: 3 additions & 1 deletion ui/src/auth/AuthGate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@
*/

import type { ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { useAuth } from './AuthContext'
import { LoginPage, NoTokenPage } from './LoginPage'

export function AuthGate({ children }: { children: ReactNode }) {
const { state } = useAuth()
const { t } = useTranslation()

if (state === 'loading') {
return (
<div className="min-h-screen flex items-center justify-center bg-bg">
<div className="text-[12px] text-text-muted">Loading…</div>
<div className="text-[12px] text-text-muted">{t('common.loading')}</div>
</div>
)
}
Expand Down
29 changes: 14 additions & 15 deletions ui/src/auth/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
*/

import { useState, useRef, useEffect, type FormEvent } from 'react'
import { useTranslation } from 'react-i18next'
import { useAuth } from './AuthContext'
import { login } from './api'

export function LoginPage() {
const { t } = useTranslation()
const { refresh } = useAuth()
const [token, setToken] = useState('')
const [error, setError] = useState<string | null>(null)
Expand All @@ -29,7 +31,7 @@ export function LoginPage() {
setBusy(true); setError(null)
const result = await login(token.trim())
if (!result.ok) {
setError(result.error ?? 'Login failed')
setError(result.error ?? t('auth.loginFailed', 'Login failed'))
setBusy(false)
return
}
Expand All @@ -40,21 +42,21 @@ export function LoginPage() {
return (
<div className="min-h-screen flex items-center justify-center bg-bg px-4">
<div className="w-full max-w-[400px] rounded-lg border border-border bg-surface px-6 py-7 shadow-sm">
<h1 className="text-[18px] font-semibold text-text mb-1">Sign in to OpenAlice</h1>
<h1 className="text-[18px] font-semibold text-text mb-1">{t('auth.signInToOpenAlice', 'Sign in to OpenAlice')}</h1>
<p className="text-[12px] text-text-muted leading-relaxed mb-5">
Paste the admin token shown on first launch.
{t('auth.pasteAdminToken', 'Paste the admin token shown on first launch.')}
{' '}
<span className="text-text-faint">
Find it in the backend logs after <code className="font-mono">pnpm dev</code> /
{' '}<code className="font-mono">docker run</code>, or rotate via
{' '}<code className="font-mono">rm data/config/auth.json</code> and restart.
{t('auth.findTokenHint', 'Find it in the backend logs after')} <code className="font-mono">pnpm dev</code> /
{' '}<code className="font-mono">docker run</code>, {t('auth.orRotateVia', 'or rotate via')}
{' '}<code className="font-mono">rm data/config/auth.json</code> {t('auth.andRestart', 'and restart')}.
</span>
</p>

<form onSubmit={onSubmit} className="space-y-3">
<div>
<label className="block text-[11px] uppercase tracking-wide text-text-muted mb-1">
Admin token
{t('auth.adminToken', 'Admin token')}
</label>
<input
ref={inputRef}
Expand All @@ -79,7 +81,7 @@ export function LoginPage() {
disabled={busy || !token.trim()}
className="btn-primary w-full justify-center"
>
{busy ? 'Signing in…' : 'Sign in'}
{busy ? t('auth.signingIn', 'Signing in\u2026') : t('auth.signIn', 'Sign in')}
</button>
</form>
</div>
Expand All @@ -88,19 +90,16 @@ export function LoginPage() {
}

export function NoTokenPage() {
const { t } = useTranslation()
return (
<div className="min-h-screen flex items-center justify-center bg-bg px-4">
<div className="w-full max-w-[460px] rounded-lg border border-border bg-surface px-6 py-7">
<h1 className="text-[18px] font-semibold text-text mb-2">No admin token configured</h1>
<h1 className="text-[18px] font-semibold text-text mb-2">{t('auth.noTokenConfigured', 'No admin token configured')}</h1>
<p className="text-[13px] text-text leading-relaxed mb-3">
The backend did not generate <code className="font-mono">data/config/auth.json</code>.
This usually means bootstrap was skipped via <code className="font-mono">OPENALICE_DISABLE_AUTH=1</code>,
or the file was created empty.
{t('auth.noTokenDescription', 'The backend did not generate data/config/auth.json. This usually means bootstrap was skipped via OPENALICE_DISABLE_AUTH=1, or the file was created empty.')}
</p>
<p className="text-[12px] text-text-muted leading-relaxed">
Stop the backend, delete <code className="font-mono">data/config/auth.json</code> if it exists,
unset <code className="font-mono">OPENALICE_DISABLE_AUTH</code>, and restart. The first-run
token will be printed to stdout.
{t('auth.noTokenInstructions', 'Stop the backend, delete data/config/auth.json if it exists, unset OPENALICE_DISABLE_AUTH, and restart. The first-run token will be printed to stdout.')}
</p>
</div>
</div>
Expand Down
66 changes: 27 additions & 39 deletions ui/src/components/ActivityBar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { type LucideIcon, MessageSquare, MessagesSquare, Inbox, Bell, LineChart, GitBranch, BarChart3, Newspaper, Zap, Settings, Code2, TerminalSquare, ChevronDown, Plug, Info } from 'lucide-react'
import { type LucideIcon, MessageSquare, MessagesSquare, Inbox, Bell, LineChart, GitBranch, BarChart3, Newspaper, Zap, Settings, Code2, TerminalSquare, ChevronDown, Plug, Landmark, Info } from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { TFunction } from 'i18next'
import { type Page } from '../App'
import { useWorkspace } from '../tabs/store'
import type { ActivitySection, ViewSpec } from '../tabs/types'
Expand All @@ -25,6 +27,7 @@ function activitySectionFor(page: Page): ActivitySection {
case 'traditional-chat': return 'traditional-chat'
case 'notifications-legacy': return 'notifications-legacy'
case 'connectors-legacy': return 'connectors-legacy'
case 'trading-accounts': return 'trading-accounts'
}
}

Expand Down Expand Up @@ -78,63 +81,46 @@ interface NavSection {
description?: string
}

const NAV_SECTIONS: NavSection[] = [
// Top — primary nav, always visible (no header, not collapsible).
// Mental model: Workspace is the atom for all work units. Chat is
// the high-frequency subset's shortcut — chat-template workspaces
// got their own top-level entry because that flow is common enough
// to warrant direct access. Workspaces (the all-templates index)
// sits alongside; the two aren't redundant: Workspaces = whole set,
// Chat = chat-shape subset shortcut.
//
// Market / News are operational tools that work but aren't load-
// bearing — they live here because they don't need lifecycle
// labelling.
function getNavSections(t: TFunction): NavSection[] {
return [
{
sectionLabel: '',
items: [
{ page: 'inbox', label: 'Inbox', icon: Inbox, defaultTab: { kind: 'inbox', params: {} } },
{ page: 'chat', label: 'Chat', icon: MessageSquare },
{ page: 'workspaces', label: 'Workspaces', icon: TerminalSquare },
{ page: 'market', label: 'Market', icon: BarChart3 },
{ page: 'news', label: 'News', icon: Newspaper, defaultTab: { kind: 'news', params: {} } },
{ page: 'inbox', label: t('inbox.title', 'Inbox'), icon: Inbox, defaultTab: { kind: 'inbox', params: {} } },
{ page: 'chat', label: t('chat.title', 'Chat'), icon: MessageSquare },
{ page: 'workspaces', label: t('workspaces.title', 'Workspaces'), icon: TerminalSquare },
{ page: 'market', label: t('market.title', 'Market'), icon: BarChart3 },
{ page: 'news', label: t('news.title', 'News'), icon: Newspaper, defaultTab: { kind: 'news', params: {} } },
],
},
// Beta — functional but unstable. The underlying cross-broker
// unification (UTA abstraction, FX/options/futures) is in active
// rearchitecture. Portfolio surfaces that state; Trading-as-Git is
// the operations side (pending broker writes). Broker connection
// CRUD lives under Settings → Trading, not here — it's a config
// surface, not a state/ops one.
{
sectionLabel: 'Beta',
description: 'Cross-broker unified state + ops surfaces. The abstraction underneath is still being settled — try them, but don\'t depend on schema or UX as stable yet. Broker connection setup lives in Settings → Trading.',
sectionLabel: t('activityBar.beta', 'Beta'),
description: t('activityBar.betaDescription', "Goal here is a unified abstraction across broker accounts (deposit/withdraw, options, futures, FX). Large engineering effort, no fixed timeline. Configure and try, but don't depend on schema or UX as stable yet."),
items: [
{ page: 'trading-as-git', label: 'Trading as Git', icon: GitBranch },
{ page: 'portfolio', label: 'Portfolio', icon: LineChart, defaultTab: { kind: 'portfolio', params: {} } },
{ page: 'trading-accounts', label: t('trading.title', 'Trading Accounts'), icon: Landmark, defaultTab: { kind: 'settings', params: { category: 'trading' } } },
{ page: 'trading-as-git', label: t('trading.tradingAsGit', 'Trading as Git'), icon: GitBranch },
{ page: 'portfolio', label: t('portfolio.title', 'Portfolio'), icon: LineChart, defaultTab: { kind: 'portfolio', params: {} } },
],
},
{
sectionLabel: 'System',
sectionLabel: t('activityBar.system', 'System'),
items: [
{ page: 'settings', label: 'Settings', icon: Settings },
{ page: 'dev', label: 'Dev', icon: Code2 },
{ page: 'settings', label: t('settings.title', 'Settings'), icon: Settings },
{ page: 'dev', label: t('dev.title', 'Dev'), icon: Code2 },
],
},
// Legacy — pre-Workspace surfaces kept around for backwards-compat
// and connector flows that can't host a CLI. Default-collapsed so
// the "this isn't the recommended path" signal is visually loud.
{
sectionLabel: 'Legacy',
sectionLabel: t('activityBar.legacy', 'Legacy'),
defaultCollapsed: true,
items: [
{ page: 'traditional-chat', label: 'Traditional chat', icon: MessagesSquare },
{ page: 'notifications-legacy', label: 'Notifications', icon: Bell, defaultTab: { kind: 'notifications-inbox', params: {} } },
{ page: 'connectors-legacy', label: 'Connectors', icon: Plug, defaultTab: { kind: 'settings', params: { category: 'connectors' } } },
{ page: 'automation', label: 'Automation', icon: Zap, defaultTab: { kind: 'automation', params: { section: 'flow' } } },
{ page: 'traditional-chat', label: t('activityBar.traditionalChat', 'Traditional chat'), icon: MessagesSquare },
{ page: 'notifications-legacy', label: t('activityBar.notifications', 'Notifications'), icon: Bell, defaultTab: { kind: 'notifications-inbox', params: {} } },
{ page: 'connectors-legacy', label: t('connectors.title', 'Connectors'), icon: Plug, defaultTab: { kind: 'settings', params: { category: 'connectors' } } },
{ page: 'automation', label: t('automation.title', 'Automation'), icon: Zap, defaultTab: { kind: 'automation', params: { section: 'flow' } } },
],
},
]
}

// ==================== ActivityBar ====================

Expand All @@ -152,6 +138,8 @@ const NAV_SECTIONS: NavSection[] = [
* communicate that. Mostly-icon view would hide the differentiation.
*/
export function ActivityBar({ open, onClose, onItemActivated }: ActivityBarProps) {
const { t } = useTranslation()
const NAV_SECTIONS = getNavSections(t)
const selectedSidebar = useWorkspace((state) => state.selectedSidebar)
const setSidebar = useWorkspace((state) => state.setSidebar)
const openOrFocus = useWorkspace((state) => state.openOrFocus)
Expand Down
Loading