oxios 1.16.0

Oxios Agent OS — Agent Operating System powered by oxi-sdk
import { useQuery } from '@tanstack/react-query'
import { useNavigate } from '@tanstack/react-router'
import { BookOpen, Brain, FolderKanban, MessageSquare, Wrench } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { api } from '@/lib/api-client'
import { useChatStore } from '@/stores/chat'
import type { Session } from '@/types'

/**
 * Empty state shown when the chat has no messages yet.
 *
 * Surfaces Oxios's 4 capabilities (knowledge / memory / tools / projects)
 * as entry points so new users know what the system can do, then lists
 * recent sessions for quick continuation.
 */
export function EmptyChatState() {
  const { t } = useTranslation()
  const navigate = useNavigate()
  const loadSession = useChatStore((s) => s.loadSession)

  const { data: sessionsData } = useQuery({
    queryKey: ['sessions-recent'],
    queryFn: () => api.get<{ items: Session[]; total: number }>('/api/sessions'),
    refetchInterval: 30_000,
  })

  const sessions: Session[] = Array.isArray(sessionsData?.items)
    ? sessionsData.items.slice(0, 8)
    : []

  const capabilities = [
    {
      key: 'knowledge',
      icon: BookOpen,
      title: t('chat.cap.knowledge', '지식'),
      desc: t('chat.cap.knowledgeDesc', '문서 검색'),
      href: '/knowledge',
      tone: 'text-info',
    },
    {
      key: 'memory',
      icon: Brain,
      title: t('chat.cap.memory', '메모리'),
      desc: t('chat.cap.memoryDesc', '시맨틱 검색'),
      href: '/memory',
      tone: 'text-primary',
    },
    {
      key: 'tools',
      icon: Wrench,
      title: t('chat.cap.tools', '도구'),
      desc: t('chat.cap.toolsDesc', 'shell·HTTP·MCP'),
      href: '/mcp',
      tone: 'text-warning',
    },
    {
      key: 'projects',
      icon: FolderKanban,
      title: t('chat.cap.projects', '프로젝트'),
      desc: t('chat.cap.projectsDesc', '연결된 컨텍스트'),
      href: '/projects',
      tone: 'text-success',
    },
  ] as const

  return (
    <div className="flex flex-col items-center gap-8 py-12 px-4">
      <div className="text-center space-y-3">
        <p className="text-lg font-semibold text-foreground">
          {t('chat.greeting', 'What can I help you with?')}
        </p>
        <p className="text-xs text-muted-foreground">
          {t('chat.subgreeting', '시작할 위치를 선택하거나 자유롭게 질문하세요')}
        </p>
      </div>

      {/* Capability grid — 4 cells, one per Oxios surface */}
      <div className="grid w-full max-w-2xl grid-cols-2 gap-2 sm:grid-cols-4">
        {capabilities.map((cap) => {
          const Icon = cap.icon
          return (
            <button
              key={cap.key}
              type="button"
              onClick={() => navigate({ to: cap.href })}
              className="group flex flex-col items-start gap-1.5 rounded-lg border bg-card p-3 text-left transition-all hover:border-primary/30 hover:shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
            >
              <span
                className={`flex h-7 w-7 items-center justify-center rounded-md bg-muted transition-colors group-hover:bg-accent`}
              >
                <Icon className={`h-4 w-4 ${cap.tone}`} />
              </span>
              <span className="text-sm font-medium leading-tight">{cap.title}</span>
              <span className="text-2xs text-muted-foreground leading-tight">{cap.desc}</span>
            </button>
          )
        })}
      </div>

      {sessions.length > 0 ? (
        <div className="w-full max-w-md space-y-1">
          <p className="text-xs font-medium text-muted-foreground mb-2">
            {t('chat.recentSessions', 'Recent conversations')}
          </p>
          <div className="space-y-1">
            {sessions.map((s) => {
              const timeStr = formatRelativeTime(s.created_at)

              return (
                <button
                  key={s.id}
                  type="button"
                  onClick={() => loadSession(s.id)}
                  className="flex items-center gap-3 w-full rounded-lg border bg-card px-3 py-2.5 text-left text-sm transition-all hover:bg-accent hover:border-primary/20 hover:shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
                >
                  <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground">
                    <MessageSquare className="h-4 w-4" />
                  </div>
                  <div className="min-w-0 flex-1">
                    <p className="truncate text-foreground">{s.title ?? `${s.id.slice(0, 8)}…`}</p>
                    <p className="text-2xs text-muted-foreground">{timeStr}</p>
                  </div>
                </button>
              )
            })}
          </div>
        </div>
      ) : null}
    </div>
  )
}

function formatRelativeTime(dateStr: string): string {
  const diff = Date.now() - new Date(dateStr).getTime()
  const minutes = Math.floor(diff / 60_000)
  if (minutes < 1) return 'just now'
  if (minutes < 60) return `${minutes}m ago`
  const hours = Math.floor(minutes / 60)
  if (hours < 24) return `${hours}h ago`
  const days = Math.floor(hours / 24)
  if (days < 7) return `${days}d ago`
  return new Date(dateStr).toLocaleDateString(undefined, {
    month: 'short',
    day: 'numeric',
  })
}