SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
14 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
4.9 KB · 90 lines tsx
Raw Blame History
1"use client";2import * as React from "react";3import Link from "next/link";4import { KeyRound, Code2, PenLine, Search, FileUp, Columns3, EyeOff } from "lucide-react";5import type { PolyModel } from "@/lib/client/types";6import { useApp } from "@/components/app/store";7import { LogoMark } from "@/components/brand/logo";8import { Button } from "@/components/ui/button";9import { CapabilityBadges } from "./model-badges";10import { Onboarding } from "@/components/app/onboarding";11import { cn } from "@/lib/utils";1213interface QuickAction {14  key: string;15  icon: React.ReactNode;16  title: string;17  hint: string;18  /** Text inserted into the composer (Write / Code / Research). */19  text?: string;20}2122const QUICK: QuickAction[] = [23  { key: "write", icon: <PenLine className="size-4" />, title: "Write", hint: "Emails, posts, docs", text: "Help me write a concise, friendly " },24  { key: "code", icon: <Code2 className="size-4" />, title: "Code", hint: "Review, debug, generate", text: "Review the following code for bugs, edge cases and readability, and suggest concrete fixes.\n\n```\n\n```" },25  { key: "research", icon: <Search className="size-4" />, title: "Research", hint: "Sources, comparisons", text: "Research the current state of " },26  { key: "file", icon: <FileUp className="size-4" />, title: "Analyze a file", hint: "PDF, CSV, image, code" },27  { key: "compare", icon: <Columns3 className="size-4" />, title: "Compare models", hint: "Same prompt, 2–4 models" },28];2930export function ChatEmptyState({ model, onPick, onAnalyzeFile, onCompare, temporary, projectName }: { model?: PolyModel; onPick: (text: string) => void; onAnalyzeFile?: () => void; onCompare?: () => void; temporary?: boolean; projectName?: string | null }) {31  const { connectedProviders, loadingModels, user, connections } = useApp();32  const noProviders = !loadingModels && connectedProviders.size === 0;33  return (34    <div className="mx-auto flex h-full w-full max-w-3xl flex-col items-center justify-center px-4 py-8 text-center sm:py-10">35      {!user.onboardingCompletedAt && connections.length === 0 ? <Onboarding /> : null}36      <LogoMark size={44} className="mb-4 rounded-xl shadow-md" />37      <h1 className="text-balance text-[22px] font-semibold tracking-tight sm:text-2xl">What do you want to work on?</h1>38      <p className="mt-1.5 max-w-md text-balance text-[14px] text-fg-muted">39        {temporary ? (40          <span className="inline-flex items-center gap-1.5">41            <EyeOff className="size-3.5" /> Temporary chat — not stored in history.42          </span>43        ) : projectName ? (44          <>45            In project <span className="font-medium text-fg">{projectName}</span>46            {model ? (47              <>48                {" "}49                · <span className="font-medium text-fg">{model.displayName}</span>50              </>51            ) : null}52          </>53        ) : model ? (54          <>55            Chatting with <span className="font-medium text-fg">{model.displayName}</span>. <span className="hidden sm:inline">Switch anytime with ⌘/.</span>56          </>57        ) : (58          "One interface. Every model. Bring your own keys."59        )}60      </p>61      {model ? <CapabilityBadges model={model} className="mt-3 justify-center" /> : null}62      {noProviders ? (63        <div className="mt-6 flex flex-col items-center gap-3 rounded-xl border border-border bg-bg-elevated p-5">64          <KeyRound className="size-5 text-accent" />65          <p className="text-sm font-medium">Connect your first provider</p>66          <p className="max-w-sm text-[13px] text-fg-muted">Add a key from OpenAI, Anthropic, Gemini, xAI, Mistral, DeepSeek, Kimi, OpenRouter or Cerebras. Keys are encrypted at rest and only decrypted server-side right before a request.</p>67          <Button asChild variant="accent" size="sm">68            <Link href="/app/settings/providers">Connect a provider</Link>69          </Button>70        </div>71      ) : (72        <div className="mt-7 grid w-full grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-5">73          {QUICK.map((q) => {74            const onClick = q.key === "file" ? onAnalyzeFile : q.key === "compare" ? onCompare : () => onPick(q.text ?? "");75            return (76              <button key={q.key} type="button" onClick={onClick} disabled={!onClick} className={cn("flex min-h-[84px] flex-col items-start gap-2 rounded-xl border border-border bg-bg-elevated p-3 text-left transition-colors hover:border-border-strong hover:bg-bg-subtle disabled:opacity-50", q.key === "compare" && "col-span-2 sm:col-span-1")}>77                <span className="flex size-8 items-center justify-center rounded-lg bg-accent-soft text-accent">{q.icon}</span>78                <span className="min-w-0">79                  <span className="block text-[13.5px] font-medium">{q.title}</span>80                  <span className="block text-[12px] text-fg-muted">{q.hint}</span>81                </span>82              </button>83            );84          })}85        </div>86      )}87    </div>88  );89}90