mirror of
https://github.com/temetro/temetro.git
synced 2026-09-04 06:45:26 +00:00
feat: AI-added records save with placeholders + "Added by AI" provenance
Stop blocking AI imports/proposals on missing non-critical fields. Records the chat agent drafts now save with safe placeholders, auto-generated file numbers, and a source="ai" marker that surfaces an "Added by AI" badge so a clinician can review/edit them later. Backend: - add `source` (manual|ai) column to patients/appointments/prescriptions (migration 0014) + canonical types, services, validation schemas - relax patient/appointment validation: empty file number allowed, demographic + type/provider/initials fall back to placeholders (initials derived from name) - patients.generateFileNumber() auto-assigns an MRN when one is missing - proposeAppointment accepts a name when no file number resolves; AI commits + /api/ai/import stamp source="ai" Frontend: - `source` on Appointment/Patient/Prescription types; AI commits send source="ai" - reusable <AiBadge> shown on the Patients table/detail and prescriptions list Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
# Task
|
||||
|
||||
A collapsible task list component for displaying AI workflow progress, with status indicators and optional descriptions.
|
||||
|
||||
The `Task` component provides a structured way to display task lists or workflow progress with collapsible details, status indicators, and progress tracking. It consists of a main `Task` container with `TaskTrigger` for the clickable header and `TaskContent` for the collapsible content area.
|
||||
|
||||
See `scripts/task.tsx` for this example.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npx ai-elements@latest add task
|
||||
```
|
||||
|
||||
## Usage with AI SDK
|
||||
|
||||
Build a mock async programming agent using [`experimental_generateObject`](/docs/reference/ai-sdk-ui/use-object).
|
||||
|
||||
Add the following component to your frontend:
|
||||
|
||||
```tsx title="app/page.tsx"
|
||||
"use client";
|
||||
|
||||
import { experimental_useObject as useObject } from "@ai-sdk/react";
|
||||
import {
|
||||
Task,
|
||||
TaskItem,
|
||||
TaskItemFile,
|
||||
TaskTrigger,
|
||||
TaskContent,
|
||||
} from "@/components/ai-elements/task";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { tasksSchema } from "@/app/api/task/route";
|
||||
import {
|
||||
SiReact,
|
||||
SiTypescript,
|
||||
SiJavascript,
|
||||
SiCss,
|
||||
SiHtml5,
|
||||
SiJson,
|
||||
SiMarkdown,
|
||||
} from "@icons-pack/react-simple-icons";
|
||||
|
||||
const iconMap = {
|
||||
react: { component: SiReact, color: "#149ECA" },
|
||||
typescript: { component: SiTypescript, color: "#3178C6" },
|
||||
javascript: { component: SiJavascript, color: "#F7DF1E" },
|
||||
css: { component: SiCss, color: "#1572B6" },
|
||||
html: { component: SiHtml5, color: "#E34F26" },
|
||||
json: { component: SiJson, color: "#000000" },
|
||||
markdown: { component: SiMarkdown, color: "#000000" },
|
||||
};
|
||||
|
||||
const TaskDemo = () => {
|
||||
const { object, submit, isLoading } = useObject({
|
||||
api: "/api/agent",
|
||||
schema: tasksSchema,
|
||||
});
|
||||
|
||||
const handleSubmit = (taskType: string) => {
|
||||
submit({ prompt: taskType });
|
||||
};
|
||||
|
||||
const renderTaskItem = (item: any, index: number) => {
|
||||
if (item?.type === "file" && item.file) {
|
||||
const iconInfo = iconMap[item.file.icon as keyof typeof iconMap];
|
||||
if (iconInfo) {
|
||||
const IconComponent = iconInfo.component;
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1" key={index}>
|
||||
{item.text}
|
||||
<TaskItemFile>
|
||||
<IconComponent
|
||||
color={item.file.color || iconInfo.color}
|
||||
className="size-4"
|
||||
/>
|
||||
<span>{item.file.name}</span>
|
||||
</TaskItemFile>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
return item?.text || "";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-6 relative size-full rounded-lg border h-[600px]">
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex gap-2 mb-6 flex-wrap">
|
||||
<Button
|
||||
onClick={() => handleSubmit("React component development")}
|
||||
disabled={isLoading}
|
||||
variant="outline"
|
||||
>
|
||||
React Development
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto space-y-4">
|
||||
{isLoading && !object && (
|
||||
<div className="text-muted-foreground">Generating tasks...</div>
|
||||
)}
|
||||
|
||||
{object?.tasks?.map((task: any, taskIndex: number) => (
|
||||
<Task key={taskIndex} defaultOpen={taskIndex === 0}>
|
||||
<TaskTrigger title={task.title || "Loading..."} />
|
||||
<TaskContent>
|
||||
{task.items?.map((item: any, itemIndex: number) => (
|
||||
<TaskItem key={itemIndex}>
|
||||
{renderTaskItem(item, itemIndex)}
|
||||
</TaskItem>
|
||||
))}
|
||||
</TaskContent>
|
||||
</Task>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskDemo;
|
||||
```
|
||||
|
||||
Add the following route to your backend:
|
||||
|
||||
```ts title="app/api/agent.ts"
|
||||
import { streamObject } from "ai";
|
||||
import { z } from "zod";
|
||||
|
||||
export const taskItemSchema = z.object({
|
||||
type: z.enum(["text", "file"]),
|
||||
text: z.string(),
|
||||
file: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
icon: z.string(),
|
||||
color: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const taskSchema = z.object({
|
||||
title: z.string(),
|
||||
items: z.array(taskItemSchema),
|
||||
status: z.enum(["pending", "in_progress", "completed"]),
|
||||
});
|
||||
|
||||
export const tasksSchema = z.object({
|
||||
tasks: z.array(taskSchema),
|
||||
});
|
||||
|
||||
// Allow streaming responses up to 30 seconds
|
||||
export const maxDuration = 30;
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { prompt } = await req.json();
|
||||
|
||||
const result = streamObject({
|
||||
model: "openai/gpt-4o",
|
||||
schema: tasksSchema,
|
||||
prompt: `You are an AI assistant that generates realistic development task workflows. Generate a set of tasks that would occur during ${prompt}.
|
||||
|
||||
Each task should have:
|
||||
- A descriptive title
|
||||
- Multiple task items showing the progression
|
||||
- Some items should be plain text, others should reference files
|
||||
- Use realistic file names and appropriate file types
|
||||
- Status should progress from pending to in_progress to completed
|
||||
|
||||
For file items, use these icon types: 'react', 'typescript', 'javascript', 'css', 'html', 'json', 'markdown'
|
||||
|
||||
Generate 3-4 tasks total, with 4-6 items each.`,
|
||||
});
|
||||
|
||||
return result.toTextStreamResponse();
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Visual icons for pending, in-progress, completed, and error states
|
||||
- Expandable content for task descriptions and additional information
|
||||
- Built-in progress counter showing completed vs total tasks
|
||||
- Optional progressive reveal of tasks with customizable timing
|
||||
- Support for custom content within task items
|
||||
- Full type safety with proper TypeScript definitions
|
||||
- Keyboard navigation and screen reader support
|
||||
|
||||
## Props
|
||||
|
||||
### `<Task />`
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `defaultOpen` | `boolean` | `true` | Whether the task is open by default. |
|
||||
| `...props` | `React.ComponentProps<typeof Collapsible>` | - | Any other props are spread to the root Collapsible component. |
|
||||
|
||||
### `<TaskTrigger />`
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `title` | `string` | Required | The title of the task that will be displayed in the trigger. |
|
||||
| `...props` | `React.ComponentProps<typeof CollapsibleTrigger>` | - | Any other props are spread to the CollapsibleTrigger component. |
|
||||
|
||||
### `<TaskContent />`
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `...props` | `React.ComponentProps<typeof CollapsibleContent>` | - | Any other props are spread to the CollapsibleContent component. |
|
||||
|
||||
### `<TaskItem />`
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `...props` | `React.ComponentProps<` | - | Any other props are spread to the underlying div. |
|
||||
|
||||
### `<TaskItemFile />`
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `...props` | `React.ComponentProps<` | - | Any other props are spread to the underlying div. |
|
||||
Reference in New Issue
Block a user