Skip to content
Draft
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
42 changes: 42 additions & 0 deletions app/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"use client"

import { RefreshCwIcon, TriangleAlertIcon } from "lucide-react"

import { Button } from "@/components/ui/button"
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty"

export default function ErrorPage({
unstable_retry,
}: {
error: Error & { digest?: string }
unstable_retry: () => void
}) {
return (
<main className="flex min-h-svh items-center justify-center p-6">
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<TriangleAlertIcon />
</EmptyMedia>
<EmptyTitle>Something went wrong</EmptyTitle>
<EmptyDescription>
The chat could not be loaded. Try the request again to recover.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button type="button" onClick={unstable_retry}>
<RefreshCwIcon data-icon="inline-start" />
Try again
</Button>
</EmptyContent>
</Empty>
</main>
)
}
30 changes: 30 additions & 0 deletions app/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty"
import { Spinner } from "@/components/ui/spinner"

export default function Loading() {
return (
<main
className="flex min-h-svh items-center justify-center p-6"
aria-live="polite"
aria-busy="true"
>
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<Spinner />
</EmptyMedia>
<EmptyTitle>Loading chat</EmptyTitle>
<EmptyDescription>
Connecting to the available models…
</EmptyDescription>
</EmptyHeader>
</Empty>
</main>
)
}
36 changes: 36 additions & 0 deletions app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Link from "next/link"
import { ArrowLeftIcon, SearchXIcon } from "lucide-react"

import { Button } from "@/components/ui/button"
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty"

export default function NotFound() {
return (
<main className="flex min-h-svh items-center justify-center p-6">
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<SearchXIcon />
</EmptyMedia>
<EmptyTitle>Page not found</EmptyTitle>
<EmptyDescription>
This address does not point to a page in the chat application.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button render={<Link href="/" aria-label="Return to chat" />}>
<ArrowLeftIcon data-icon="inline-start" />
Return to chat
</Button>
</EmptyContent>
</Empty>
</main>
)
}
34 changes: 26 additions & 8 deletions components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export function Chat({
children?: React.ReactNode
}) {
const [model, setModel] = React.useState(models[0]?.id ?? "")
const [isAnswering, setIsAnswering] = React.useState(false)

const {
messages,
Expand Down Expand Up @@ -67,6 +68,24 @@ export function Chat({
)
: undefined

async function handleAnswer(
toolCallId: string,
answer: { question: string; answer: string }[]
) {
if (isAnswering) return

setIsAnswering(true)
try {
await addToolOutput({
tool: "ask_user",
toolCallId,
output: answer,
})
} finally {
setIsAnswering(false)
}
}

return (
<ChatActionsProvider isBusy={isBusy} onNewChat={() => setMessages([])}>
<div className="mx-auto flex h-svh w-full flex-col">
Expand Down Expand Up @@ -112,7 +131,11 @@ export function Chat({
))}
{status === "submitted" && (
<MessageScrollerItem messageId="thinking">
<div className="flex shimmer items-center gap-2 px-3 text-sm text-muted-foreground">
<div
className="flex shimmer items-center gap-2 px-3 text-sm text-muted-foreground"
role="status"
aria-live="polite"
>
Thinking…
</div>
</MessageScrollerItem>
Expand All @@ -121,13 +144,8 @@ export function Chat({
{pendingQuestion && (
<QuestionCard
part={pendingQuestion}
onAnswer={(toolCallId, answer) =>
addToolOutput({
tool: "ask_user",
toolCallId,
output: answer,
})
}
isPending={isAnswering}
onAnswer={handleAnswer}
/>
)}
</MessageScrollerViewport>
Expand Down
3 changes: 3 additions & 0 deletions components/model-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ export function ModelSelect({
models,
value,
onValueChange,
disabled = false,
}: {
models: GatewayModel[]
value: string
onValueChange: (value: string) => void
disabled?: boolean
}) {
const items = React.useMemo(
() => models.map((model) => ({ label: model.name, value: model.id })),
Expand All @@ -30,6 +32,7 @@ export function ModelSelect({
<Select
items={items}
value={value}
disabled={disabled}
onValueChange={(next) => {
if (typeof next === "string") onValueChange(next)
}}
Expand Down
1 change: 1 addition & 0 deletions components/parts/github-repo-part.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export function GithubRepoPart({ part }: { part: GithubRepoToolPart }) {
href={part.output.url}
target="_blank"
rel="noreferrer"
aria-label="Open repository details on GitHub"
className="flex w-fit items-center gap-3 px-1.5 text-sm text-muted-foreground hover:text-foreground"
>
<span className="font-medium text-foreground">
Expand Down
7 changes: 6 additions & 1 deletion components/parts/sources-part.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,12 @@ export function SourcesPart({ parts }: { parts: ChatMessagePart[] }) {
size="sm"
className="rounded-xl"
render={
<a href={source.url} target="_blank" rel="noreferrer" />
<a
href={source.url}
target="_blank"
rel="noreferrer"
aria-label={`Open ${title} from ${hostname}`}
/>
}
role="listitem"
>
Expand Down
8 changes: 7 additions & 1 deletion components/prompt-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,17 @@ export function PromptForm({
}

return (
<form onSubmit={handleSubmit}>
<form onSubmit={handleSubmit} aria-busy={isBusy}>
<InputGroup>
<InputGroupTextarea
id="chat-prompt"
name="message"
required
minLength={1}
placeholder="Send a message…"
className="p-3.5"
value={input}
disabled={isBusy}
onChange={(event) => setInput(event.target.value)}
onKeyDown={(event) => {
if (
Expand All @@ -61,6 +66,7 @@ export function PromptForm({
models={models}
value={model}
onValueChange={onModelChange}
disabled={isBusy}
/>
{isBusy ? (
<InputGroupButton
Expand Down
25 changes: 21 additions & 4 deletions components/question-card.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client"

import { type AskUserToolPart } from "@/lib/tools"
import { Spinner } from "@/components/ui/spinner"
import {
Questionnaire,
QuestionnaireActions,
Expand All @@ -18,13 +19,15 @@ import {

export function QuestionCard({
part,
isPending,
onAnswer,
}: {
part: AskUserToolPart
isPending: boolean
onAnswer: (
toolCallId: string,
answers: { question: string; answer: string }[]
) => void
) => Promise<void>
}) {
const questions = part.state === "input-available" ? part.input.questions : []

Expand All @@ -39,13 +42,15 @@ export function QuestionCard({
<Questionnaire
key={part.toolCallId}
defaultItem="q0"
aria-busy={isPending}
items={questions.map((question, index) => ({
choices: question.choices.map((choice) => ({ value: choice })),
name: `q${index}`,
required: true,
}))}
onSubmit={(event) => {
event.preventDefault()
if (isPending) return
const formData = new FormData(event.currentTarget)
onAnswer(
part.toolCallId,
Expand Down Expand Up @@ -77,9 +82,21 @@ export function QuestionCard({
</QuestionnaireItem>
))}
<QuestionnaireActions>
<QuestionnairePrevious />
<QuestionnaireNext>Next</QuestionnaireNext>
<QuestionnaireSubmit>Answer</QuestionnaireSubmit>
<span className="sr-only" role="status" aria-live="polite">
{isPending ? "Sending answer" : ""}
</span>
<QuestionnairePrevious disabled={isPending} />
<QuestionnaireNext disabled={isPending}>Next</QuestionnaireNext>
<QuestionnaireSubmit disabled={isPending}>
{isPending ? (
<>
<Spinner />
Sending…
</>
) : (
"Answer"
)}
</QuestionnaireSubmit>
</QuestionnaireActions>
</Questionnaire>
)}
Expand Down
Binary file added docs/assets/shadscan-76-score.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.