After reading the Heicode-接口契约文档 v2.2 in
~/Desktop/taijigit/taiji-AI-PAD/Docs/ the right data sources are clear:
§1 /api/auth/* — already wired (features/auth/api.ts)
§4 /api/user/heicode/* — model balance + usage + logs
§5 /api/agnet/* — Agnet platform stub (deployments / audit / etc)
§6 /api/user/tasks/* — HeicodeTask intent → followups → card
The first wave UI work (commit 2df233b) used Heicode-local controllers
as the data source (AgnetDeployment.orchestration_plan as a stand-in
for the task object). That was wrong — the contract document is clear
that HeicodeTask (§6) is the canonical user-facing task object, and the
mcp-server stub at §5 is the canonical deployment source.
This commit redirects the data plumbing without touching the UI shells:
1. New lib/heicode-mcp.ts — typed client that calls mcp-server through
the existing same-origin /api/heicode-auth/* proxy. Implements the
subset of §4/§5/§6 the Manager UI needs:
createTaskFromIntent / listHeicodeTasks / getHeicodeTask / answer
getHeicodeBalance / getHeicodeModels / getHeicodeUsage / getHeicodeLogs
listMcpAgnetDeployments / listMcpAuditLogs
2. HomeHero (features/dashboard/components/home-hero.tsx):
- Idea input now POSTs /api/user/tasks/intent and routes the user
to /tasks/$id once the server returns the new task with its first
round of follow-ups. Previously it only stashed the idea in
localStorage which the docs §10 didn't actually require.
- ContinueTasks + TodayFocus now consume listHeicodeTasks output
(HeicodeTask.status / status_caption / updated_at:ms) instead of
AgnetDeployment shape.
3. TaskCardView (features/tasks/task-card-view.tsx):
- Reads getHeicodeTask(id) from mcp-server (refetch every 15s).
- When status=configuring renders the open follow-ups from the most
recent heicode thread entry as clickable option chips; clicking
POSTs answer to /api/user/tasks/$id/answer and the server-side
state machine advances. high-risk options get a red badge per §6.
- When status=running (followups answered, card materialised) the
four blocks docs §10 任务卡 mandates are rendered from task.card:
目标 / 第一版范围 / 自动生成 / 待确认上下文.
4. AgnetAuditPage (features/agnet-console/pages.tsx):
- Switched queryFn from local getAgnetAuditLogs to mcp-server
listMcpAuditLogs. The redacted-card renderer already accepts any
{resource_id, allowed_actions, constraints, secret_ref} shape so
no UI change needed; banner still announces no plaintext.
Notes:
- /wallet refactor to §4 deferred — it pulls multiple legacy series
from the local NewAPI controllers and the rewrite is a separate
pass. Manager users see local data for now; the call is identical
shape so swap is mechanical once we get there.
- Local TS check clean. Not deployed.
- Earlier 2df233b's UI structures (HomeHero shape, TaskCard layout,
recommendation dialog, audit redacted view) stay verbatim — only
the data fetching layer moved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
540 lines
18 KiB
TypeScript
Vendored
540 lines
18 KiB
TypeScript
Vendored
/**
|
||
* Heicode 首页主视图 — 实现 docs/product-package/10-frontend-detail-spec.md
|
||
* 「初始首页」+ 11-product-prototype-wireframes.md §4 Manager 辅助控制台原型。
|
||
*
|
||
* 四块对应 §10:
|
||
* 主输入 — "你想把什么想法变成可以上线的软件?"
|
||
* 继续任务 — 最近任务、运行状态、待审批提示
|
||
* 今日焦点 — 当前最重要任务、失败任务、待确认事项
|
||
* 辅助入口 — 客户端下载、账户安全、准备清单、最近审计
|
||
*
|
||
* 设计原则(§10 §"设计原则" 1-8):
|
||
* 1. 第一屏只强调"你想做什么"
|
||
* 2. 主流程围绕当前任务,不围绕后台模块
|
||
* 8. 状态必须覆盖空态、加载、错误、成功
|
||
*
|
||
* Manager 不承担主开发对话(§13 §5.1)— 主输入只把想法暂存到 localStorage
|
||
* 草稿,提示用户打开客户端继续推进。后端 /api/task 之类的 idea-task 接口
|
||
* 还没接,等接通后这个 stash 流程换成 POST /api/task。
|
||
*/
|
||
import { useCallback, useMemo, useRef, useState } from 'react'
|
||
import { Link, useNavigate } from '@tanstack/react-router'
|
||
import { useQuery } from '@tanstack/react-query'
|
||
import { useTranslation } from 'react-i18next'
|
||
import {
|
||
ArrowRight,
|
||
ArrowUpRight,
|
||
CheckCircle2,
|
||
CircleDashed,
|
||
Cpu,
|
||
Download,
|
||
GitBranch,
|
||
PlayCircle,
|
||
Rocket,
|
||
ShieldCheck,
|
||
Sparkles,
|
||
UserCog,
|
||
XCircle,
|
||
} from 'lucide-react'
|
||
import { Button } from '@/components/ui/button'
|
||
import { Skeleton } from '@/components/ui/skeleton'
|
||
import { cn } from '@/lib/utils'
|
||
import {
|
||
createTaskFromIntent,
|
||
listHeicodeTasks,
|
||
type HeicodeTask,
|
||
} from '@/lib/heicode-mcp'
|
||
import { toast as sonnerToast } from 'sonner'
|
||
|
||
const DRAFT_STORAGE_KEY = 'heicode_idea_draft'
|
||
|
||
type StatusKey = 'running' | 'success' | 'failed' | 'pending'
|
||
|
||
const STATUS_MAP: Record<string, StatusKey> = {
|
||
running: 'running',
|
||
active: 'running',
|
||
in_progress: 'running',
|
||
succeeded: 'success',
|
||
success: 'success',
|
||
completed: 'success',
|
||
failed: 'failed',
|
||
error: 'failed',
|
||
rejected: 'failed',
|
||
pending: 'pending',
|
||
queued: 'pending',
|
||
awaiting: 'pending',
|
||
}
|
||
|
||
function classifyStatus(s: string): StatusKey {
|
||
return STATUS_MAP[(s || '').toLowerCase()] ?? 'pending'
|
||
}
|
||
|
||
function StatusBadge({ phase }: { phase: string }) {
|
||
const k = classifyStatus(phase)
|
||
const palette: Record<StatusKey, { cls: string; Icon: typeof PlayCircle }> = {
|
||
running: {
|
||
cls: 'bg-[color-mix(in_oklch,var(--primary)_22%,transparent)] text-primary ring-primary/40',
|
||
Icon: PlayCircle,
|
||
},
|
||
success: {
|
||
cls: 'bg-emerald-500/15 text-emerald-400 ring-emerald-500/30',
|
||
Icon: CheckCircle2,
|
||
},
|
||
failed: {
|
||
cls: 'bg-rose-500/15 text-rose-400 ring-rose-500/30',
|
||
Icon: XCircle,
|
||
},
|
||
pending: {
|
||
cls: 'bg-amber-500/15 text-amber-400 ring-amber-500/30',
|
||
Icon: CircleDashed,
|
||
},
|
||
}
|
||
const p = palette[k]
|
||
return (
|
||
<span
|
||
className={cn(
|
||
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.12em] ring-1 ring-inset',
|
||
p.cls
|
||
)}
|
||
>
|
||
<p.Icon className='h-3 w-3' />
|
||
{phase || 'pending'}
|
||
</span>
|
||
)
|
||
}
|
||
|
||
function formatRelativeTimeMs(ms?: number): string {
|
||
if (!ms) return '—'
|
||
const diff = Date.now() - ms
|
||
if (diff < 0) return '—'
|
||
const sec = Math.round(diff / 1000)
|
||
if (sec < 60) return `${sec}s`
|
||
const min = Math.round(sec / 60)
|
||
if (min < 60) return `${min}m`
|
||
const hr = Math.round(min / 60)
|
||
if (hr < 24) return `${hr}h`
|
||
return `${Math.round(hr / 24)}d`
|
||
}
|
||
|
||
function IdeaInput({ t }: { t: ReturnType<typeof useTranslation>['t'] }) {
|
||
const [value, setValue] = useState<string>(() => {
|
||
if (typeof window === 'undefined') return ''
|
||
return window.localStorage.getItem(DRAFT_STORAGE_KEY) ?? ''
|
||
})
|
||
const [submitting, setSubmitting] = useState(false)
|
||
const taRef = useRef<HTMLTextAreaElement | null>(null)
|
||
const navigate = useNavigate()
|
||
|
||
// POST /api/user/tasks/intent on the mcp-server. Backend returns a new
|
||
// HeicodeTask with status=configuring and a thread containing the first
|
||
// round of follow-up questions. We navigate to /tasks/$id so the user
|
||
// can answer them — that's where the recommendation summary materialises
|
||
// (status flips to running once all followups answered, see §6.4).
|
||
const handleSubmit = useCallback(async () => {
|
||
const trimmed = value.trim()
|
||
if (!trimmed) {
|
||
taRef.current?.focus()
|
||
return
|
||
}
|
||
try {
|
||
window.localStorage.setItem(DRAFT_STORAGE_KEY, trimmed)
|
||
} catch {
|
||
/* localStorage unavailable — ignore */
|
||
}
|
||
setSubmitting(true)
|
||
try {
|
||
const task = await createTaskFromIntent(trimmed)
|
||
sonnerToast.success(t('Task drafted. Answer the follow-ups to build the recommendation.'))
|
||
void navigate({ to: '/tasks/$id', params: { id: task.id } })
|
||
} catch (err) {
|
||
sonnerToast.error(err instanceof Error ? err.message : t('Failed to create task'))
|
||
} finally {
|
||
setSubmitting(false)
|
||
}
|
||
}, [value, t, navigate])
|
||
|
||
return (
|
||
<section
|
||
className='relative overflow-hidden rounded-3xl border p-6 sm:p-8'
|
||
style={{
|
||
borderColor: 'rgba(123,107,227,0.28)',
|
||
backgroundColor: 'rgba(123,107,227,0.04)',
|
||
backgroundImage:
|
||
'radial-gradient(circle at 0% 0%, rgba(184,136,229,0.16), transparent 60%), radial-gradient(circle at 100% 100%, rgba(107,124,224,0.14), transparent 55%)',
|
||
}}
|
||
>
|
||
<div className='relative z-10 flex flex-col gap-5'>
|
||
<div className='flex items-start gap-3'>
|
||
<span
|
||
className='inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl text-white shadow-[0_10px_30px_-12px_rgba(123,107,227,0.6)]'
|
||
style={{ backgroundImage: 'var(--gradient-brand)' }}
|
||
>
|
||
<Sparkles className='h-5 w-5' />
|
||
</span>
|
||
<div className='min-w-0 flex-1'>
|
||
<h2
|
||
className='text-2xl font-semibold tracking-tight sm:text-3xl'
|
||
style={{
|
||
backgroundImage: 'var(--gradient-brand)',
|
||
WebkitBackgroundClip: 'text',
|
||
backgroundClip: 'text',
|
||
color: 'transparent',
|
||
}}
|
||
>
|
||
{t('What idea would you like to turn into shippable software?')}
|
||
</h2>
|
||
<p className='mt-2 text-sm text-muted-foreground'>
|
||
{t(
|
||
'Describe the product, feature, or fix. Heicode will draft the task, recommend resources, and dispatch the Agnet team.'
|
||
)}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<textarea
|
||
ref={taRef}
|
||
value={value}
|
||
onChange={(e) => setValue(e.target.value)}
|
||
rows={5}
|
||
spellCheck={false}
|
||
placeholder={t(
|
||
'e.g. A task-management SaaS for small teams: login, projects, tasks, comments, notifications, deploy to Azure.'
|
||
)}
|
||
className='w-full resize-none rounded-2xl border px-4 py-3 text-sm leading-relaxed shadow-[inset_0_1px_0_rgba(123,107,227,0.10)] outline-none transition focus:ring-2 focus:ring-[color-mix(in_oklch,var(--primary)_40%,transparent)]'
|
||
style={{
|
||
borderColor: 'rgba(123,107,227,0.22)',
|
||
backgroundColor: 'rgba(255,255,255,0.04)',
|
||
}}
|
||
onKeyDown={(e) => {
|
||
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
||
e.preventDefault()
|
||
handleSubmit()
|
||
}
|
||
}}
|
||
/>
|
||
|
||
<div className='flex flex-wrap items-center justify-between gap-3'>
|
||
<p className='text-xs text-muted-foreground'>
|
||
{t(
|
||
'Heicode Manager only captures the idea. The main task conversation happens in the desktop client.'
|
||
)}
|
||
</p>
|
||
<Button
|
||
type='button'
|
||
onClick={handleSubmit}
|
||
className='h-10 gap-2 rounded-xl px-5 text-sm font-semibold text-white shadow-[0_18px_48px_-18px_rgba(123,107,227,0.65)] transition-transform hover:translate-y-[-1px] active:translate-y-0 active:scale-[0.99]'
|
||
style={{
|
||
backgroundImage: 'var(--gradient-brand-btn)',
|
||
border: '1px solid rgba(255,255,255,0.16)',
|
||
}}
|
||
>
|
||
<Sparkles className='h-4 w-4' />
|
||
{t('Save idea')}
|
||
<span className='ms-1 hidden text-[10px] font-medium opacity-80 sm:inline'>
|
||
{t('⌘/Ctrl + Enter')}
|
||
</span>
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
function ContinueTasks({
|
||
tasks,
|
||
isLoading,
|
||
t,
|
||
}: {
|
||
tasks: HeicodeTask[]
|
||
isLoading: boolean
|
||
t: ReturnType<typeof useTranslation>['t']
|
||
}) {
|
||
const recent = useMemo(() => {
|
||
const sorted = [...tasks].sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0))
|
||
return sorted.slice(0, 4)
|
||
}, [tasks])
|
||
|
||
return (
|
||
<section className='rounded-2xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_60%,transparent)] p-5'>
|
||
<header className='mb-4 flex items-center justify-between'>
|
||
<div>
|
||
<p className='text-[11px] font-semibold tracking-[0.16em] text-muted-foreground uppercase'>
|
||
{t('Continue working')}
|
||
</p>
|
||
<h3 className='mt-0.5 text-base font-semibold'>
|
||
{t('Recent tasks')}
|
||
</h3>
|
||
</div>
|
||
<Button
|
||
asChild
|
||
variant='ghost'
|
||
size='sm'
|
||
className='gap-1 text-primary'
|
||
>
|
||
<Link to='/deployments'>
|
||
{t('All tasks')}
|
||
<ArrowUpRight className='h-3.5 w-3.5' />
|
||
</Link>
|
||
</Button>
|
||
</header>
|
||
{isLoading ? (
|
||
<div className='space-y-2'>
|
||
{Array.from({ length: 3 }).map((_, i) => (
|
||
<Skeleton key={i} className='h-16 rounded-xl' />
|
||
))}
|
||
</div>
|
||
) : recent.length === 0 ? (
|
||
<p className='rounded-xl border border-dashed border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] bg-background/40 p-4 text-center text-xs text-muted-foreground'>
|
||
{t(
|
||
'No tasks yet. Capture an idea above, then confirm the recommendation in the desktop client to launch Agnet.'
|
||
)}
|
||
</p>
|
||
) : (
|
||
<ul className='space-y-2'>
|
||
{recent.map((task) => (
|
||
<li key={task.id}>
|
||
<Link
|
||
to='/tasks/$id'
|
||
params={{ id: task.id }}
|
||
className='flex items-start justify-between gap-3 rounded-xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-background/40 p-3 transition hover:-translate-y-px hover:border-primary/40 hover:shadow-[0_18px_48px_-32px_rgba(123,107,227,0.4)]'
|
||
>
|
||
<div className='min-w-0 flex-1'>
|
||
<p className='line-clamp-1 text-sm font-medium'>
|
||
{task.name || task.intent || t('Untitled task')}
|
||
</p>
|
||
<p className='mt-1 text-[11px] text-muted-foreground'>
|
||
{task.status_caption || task.status}
|
||
{' · '}
|
||
{t('Updated')}{' '}
|
||
{formatRelativeTimeMs(task.updated_at)} {t('ago')}
|
||
</p>
|
||
</div>
|
||
<StatusBadge phase={task.status} />
|
||
</Link>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</section>
|
||
)
|
||
}
|
||
|
||
function TodayFocus({
|
||
tasks,
|
||
isLoading,
|
||
t,
|
||
}: {
|
||
tasks: HeicodeTask[]
|
||
isLoading: boolean
|
||
t: ReturnType<typeof useTranslation>['t']
|
||
}) {
|
||
const buckets = useMemo(() => {
|
||
const failed = tasks.filter((task) => classifyStatus(task.status) === 'failed')
|
||
// "Pending confirmation" maps to HeicodeTask.status = 'configuring'
|
||
// (followups not all answered yet) AND tasks awaiting_approval.
|
||
const pending = tasks.filter(
|
||
(task) =>
|
||
task.status === 'configuring' || task.status === 'awaiting_approval'
|
||
)
|
||
const running = tasks.filter((task) => classifyStatus(task.status) === 'running')
|
||
return { failed, pending, running }
|
||
}, [tasks])
|
||
|
||
const focusItems: Array<{
|
||
Icon: typeof XCircle
|
||
tone: 'failed' | 'pending' | 'running' | 'idle'
|
||
title: string
|
||
count: number
|
||
hint: string
|
||
}> = [
|
||
{
|
||
Icon: XCircle,
|
||
tone: 'failed',
|
||
title: t('Failed tasks'),
|
||
count: buckets.failed.length,
|
||
hint: t('Review and decide next action'),
|
||
},
|
||
{
|
||
Icon: CircleDashed,
|
||
tone: 'pending',
|
||
title: t('Pending confirmation'),
|
||
count: buckets.pending.length,
|
||
hint: t('Awaiting recommendation summary review'),
|
||
},
|
||
{
|
||
Icon: PlayCircle,
|
||
tone: 'running',
|
||
title: t('Running'),
|
||
count: buckets.running.length,
|
||
hint: t('Active Agnet sub-loops'),
|
||
},
|
||
]
|
||
|
||
const toneCls: Record<'failed' | 'pending' | 'running' | 'idle', string> = {
|
||
failed: 'text-rose-400 bg-rose-500/10 ring-rose-500/25',
|
||
pending: 'text-amber-400 bg-amber-500/10 ring-amber-500/25',
|
||
running: 'text-primary bg-[color-mix(in_oklch,var(--primary)_18%,transparent)] ring-primary/30',
|
||
idle: 'text-muted-foreground bg-muted/40 ring-border',
|
||
}
|
||
|
||
return (
|
||
<section className='rounded-2xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_60%,transparent)] p-5'>
|
||
<header className='mb-4'>
|
||
<p className='text-[11px] font-semibold tracking-[0.16em] text-muted-foreground uppercase'>
|
||
{t("Today's focus")}
|
||
</p>
|
||
<h3 className='mt-0.5 text-base font-semibold'>
|
||
{t('What needs your attention')}
|
||
</h3>
|
||
</header>
|
||
{isLoading ? (
|
||
<div className='grid gap-2 sm:grid-cols-3'>
|
||
{Array.from({ length: 3 }).map((_, i) => (
|
||
<Skeleton key={i} className='h-20 rounded-xl' />
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className='grid gap-3 sm:grid-cols-3'>
|
||
{focusItems.map((it) => (
|
||
<div
|
||
key={it.title}
|
||
className='rounded-xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-background/40 p-3'
|
||
>
|
||
<div className='flex items-center gap-2'>
|
||
<span
|
||
className={cn(
|
||
'inline-flex h-7 w-7 items-center justify-center rounded-lg ring-1 ring-inset',
|
||
toneCls[it.tone]
|
||
)}
|
||
>
|
||
<it.Icon className='h-3.5 w-3.5' />
|
||
</span>
|
||
<p className='text-xs font-medium text-muted-foreground'>
|
||
{it.title}
|
||
</p>
|
||
</div>
|
||
<p className='mt-2 text-2xl font-semibold'>{it.count}</p>
|
||
<p className='mt-0.5 text-[11px] text-muted-foreground'>
|
||
{it.hint}
|
||
</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</section>
|
||
)
|
||
}
|
||
|
||
function HelperEntries({
|
||
t,
|
||
}: {
|
||
t: ReturnType<typeof useTranslation>['t']
|
||
}) {
|
||
const entries: Array<{
|
||
Icon: typeof GitBranch
|
||
title: string
|
||
desc: string
|
||
to: string
|
||
}> = [
|
||
{
|
||
Icon: GitBranch,
|
||
title: t('Preparation checklist'),
|
||
desc: t('Connect code, SK, docs and cloud accounts'),
|
||
to: '/sk-sources',
|
||
},
|
||
{
|
||
Icon: Download,
|
||
title: t('Heicode desktop client'),
|
||
desc: t('Download macOS / Windows builds'),
|
||
to: '/desktop-client',
|
||
},
|
||
{
|
||
Icon: Cpu,
|
||
title: t('Models and balance'),
|
||
desc: t('Available models, quota and recent usage'),
|
||
to: '/wallet',
|
||
},
|
||
{
|
||
Icon: UserCog,
|
||
title: t('Account security'),
|
||
desc: t('Tokens, password and active sessions'),
|
||
to: '/profile',
|
||
},
|
||
{
|
||
Icon: ShieldCheck,
|
||
title: t('Recent audit'),
|
||
desc: t('Approvals, scope changes and credential rotations'),
|
||
to: '/audit',
|
||
},
|
||
{
|
||
Icon: Rocket,
|
||
title: t('Task overview'),
|
||
desc: t('Status of every Agnet task you launched'),
|
||
to: '/deployments',
|
||
},
|
||
]
|
||
|
||
return (
|
||
<section>
|
||
<header className='mb-3'>
|
||
<p className='text-[11px] font-semibold tracking-[0.16em] text-muted-foreground uppercase'>
|
||
{t('Helpers')}
|
||
</p>
|
||
<h3 className='mt-0.5 text-base font-semibold'>
|
||
{t('Other things you can do')}
|
||
</h3>
|
||
</header>
|
||
<div className='grid gap-3 sm:grid-cols-2 lg:grid-cols-3'>
|
||
{entries.map((e) => (
|
||
<Link
|
||
key={e.to}
|
||
to={e.to}
|
||
className='group flex items-start gap-3 rounded-2xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_55%,transparent)] p-4 transition hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-[0_18px_48px_-32px_rgba(123,107,227,0.55)]'
|
||
>
|
||
<span
|
||
className='inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-xl text-primary'
|
||
style={{
|
||
backgroundColor: 'rgba(123,107,227,0.12)',
|
||
border: '1px solid rgba(123,107,227,0.22)',
|
||
}}
|
||
>
|
||
<e.Icon className='h-4 w-4' />
|
||
</span>
|
||
<div className='min-w-0 flex-1'>
|
||
<p className='flex items-center gap-1 text-sm font-medium'>
|
||
{e.title}
|
||
<ArrowRight className='h-3.5 w-3.5 -translate-x-0.5 opacity-0 transition group-hover:translate-x-0 group-hover:opacity-70' />
|
||
</p>
|
||
<p className='mt-0.5 text-xs text-muted-foreground'>{e.desc}</p>
|
||
</div>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
export function HomeHeroView() {
|
||
const { t } = useTranslation()
|
||
const { data, isLoading } = useQuery({
|
||
queryKey: ['heicode', 'tasks', 'recent'],
|
||
queryFn: () => listHeicodeTasks({ limit: 20 }),
|
||
refetchInterval: 60_000,
|
||
// mcp-server may be unreachable while the JWT bridge isn't established.
|
||
// Treat that as empty list instead of a hard error in the hero.
|
||
retry: false,
|
||
})
|
||
const tasks = data?.items ?? []
|
||
|
||
return (
|
||
<div className='space-y-5'>
|
||
<IdeaInput t={t} />
|
||
<div className='grid gap-5 lg:grid-cols-[minmax(0,1.05fr)_minmax(0,1fr)]'>
|
||
<ContinueTasks tasks={tasks} isLoading={isLoading} t={t} />
|
||
<TodayFocus tasks={tasks} isLoading={isLoading} t={t} />
|
||
</div>
|
||
<HelperEntries t={t} />
|
||
</div>
|
||
)
|
||
}
|