changed FRONTEND and docker configs
This commit is contained in:
parent
1c90b3d75f
commit
f2123a5779
@ -1,19 +1,50 @@
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
.venv
|
||||
venv
|
||||
.env
|
||||
env/
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.py[cod]
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
*.so
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Virtual envs
|
||||
.venv
|
||||
venv
|
||||
env/
|
||||
|
||||
# Env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Editors
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Tests
|
||||
tests/
|
||||
test/
|
||||
README.md
|
||||
docker-compose.yml
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
frontend/node_modules/
|
||||
.next/
|
||||
frontend/.next/
|
||||
npm-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
.dockerignore
|
||||
|
||||
|
||||
.files/
|
||||
.chainlit/translations/*
|
||||
!.chainlit/translations/en-US.json
|
||||
.files/
|
||||
!.chainlit/translations/en-US.json
|
||||
0
backend/__init__.py
Normal file
0
backend/__init__.py
Normal file
@ -12,4 +12,4 @@ COPY backend/ ./backend/
|
||||
|
||||
EXPOSE 8001
|
||||
|
||||
CMD ["python", "-m", "mcp_server.server"]
|
||||
CMD ["python", "-m", "backend.mcp_server.mcp_server"]
|
||||
21
compose.yaml
21
compose.yaml
@ -1,9 +1,22 @@
|
||||
name: "legal-ai-assistant"
|
||||
services:
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_started
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: backend/Dockerfile
|
||||
dockerfile: /backend/Dockerfile
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8000:8000"
|
||||
@ -26,18 +39,18 @@ services:
|
||||
ports:
|
||||
- "4000:4000"
|
||||
env_file:
|
||||
- .env
|
||||
- backend/.env
|
||||
environment:
|
||||
- GROQ_API_KEY=${GROQ_API_KEY}
|
||||
- GEMINI_API_KEY=${GEMINI_API_KEY}
|
||||
volumes:
|
||||
- ./config.yaml:/app/config.yaml:ro
|
||||
- ./backend/config.yaml:/app/config.yaml:ro
|
||||
command: ['--config', '/app/config.yaml', '--port', '4000']
|
||||
|
||||
mcp:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: backend/mcp_server/Dockerfile
|
||||
dockerfile: /backend/mcp_server/Dockerfile
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8001:8001"
|
||||
|
||||
14
frontend/Dockerfile
Normal file
14
frontend/Dockerfile
Normal file
@ -0,0 +1,14 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["npm", "start"]
|
||||
@ -1,31 +1,19 @@
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { frontendTools } from "@assistant-ui/react-ai-sdk";
|
||||
import {
|
||||
JSONSchema7,
|
||||
streamText,
|
||||
convertToModelMessages,
|
||||
type UIMessage,
|
||||
} from "ai";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const {
|
||||
messages,
|
||||
system,
|
||||
tools,
|
||||
}: {
|
||||
messages: UIMessage[];
|
||||
system?: string;
|
||||
tools?: Record<string, { description?: string; parameters: JSONSchema7 }>;
|
||||
} = await req.json();
|
||||
const body = await req.json();
|
||||
|
||||
const result = streamText({
|
||||
model: openai("gpt-5-nano"),
|
||||
messages: await convertToModelMessages(messages),
|
||||
system,
|
||||
tools: {
|
||||
...frontendTools(tools ?? {}),
|
||||
const response = await fetch("http://backend:8000/api/chat", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
return result.toUIMessageStreamResponse();
|
||||
}
|
||||
return new Response(response.body, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
});
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 286 KiB |
@ -47,50 +47,45 @@
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.21 0.006 285.885);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--background: oklch(0.12 0.04 240);
|
||||
--foreground: oklch(0.95 0.01 240);
|
||||
--card: oklch(0.17 0.05 240);
|
||||
--card-foreground: oklch(0.95 0.01 240);
|
||||
--popover: oklch(0.17 0.05 240);
|
||||
--popover-foreground: oklch(0.95 0.01 240);
|
||||
--primary: oklch(0.6 0.18 240);
|
||||
--primary-foreground: oklch(0.98 0 0);
|
||||
--secondary: oklch(0.2 0.05 240);
|
||||
--secondary-foreground: oklch(0.95 0.01 240);
|
||||
--muted: oklch(0.2 0.05 240);
|
||||
--muted-foreground: oklch(0.65 0.05 240);
|
||||
--accent: oklch(0.22 0.06 240);
|
||||
--accent-foreground: oklch(0.95 0.01 240);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.705 0.015 286.067);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--border: oklch(1 0 0 / 8%);
|
||||
--input: oklch(1 0 0 / 12%);
|
||||
--ring: oklch(0.5 0.15 240);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.92 0.004 286.32);
|
||||
--primary-foreground: oklch(0.21 0.006 285.885);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--background: oklch(0.12 0.04 240);
|
||||
--foreground: oklch(0.95 0.01 240);
|
||||
--card: oklch(0.17 0.05 240);
|
||||
--card-foreground: oklch(0.95 0.01 240);
|
||||
--popover: oklch(0.17 0.05 240);
|
||||
--popover-foreground: oklch(0.95 0.01 240);
|
||||
--primary: oklch(0.6 0.18 240);
|
||||
--primary-foreground: oklch(0.98 0 0);
|
||||
--secondary: oklch(0.2 0.05 240);
|
||||
--secondary-foreground: oklch(0.95 0.01 240);
|
||||
--muted: oklch(0.2 0.05 240);
|
||||
--muted-foreground: oklch(0.65 0.05 240);
|
||||
--accent: oklch(0.22 0.06 240);
|
||||
--accent-foreground: oklch(0.95 0.01 240);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.552 0.016 285.938);
|
||||
--border: oklch(1 0 0 / 8%);
|
||||
--input: oklch(1 0 0 / 12%);
|
||||
--ring: oklch(0.5 0.15 240);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
|
||||
@ -1,222 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { PropsWithChildren, useEffect, useState, type FC } from "react";
|
||||
import { XIcon, PlusIcon, FileText } from "lucide-react";
|
||||
import {
|
||||
AttachmentPrimitive,
|
||||
ComposerPrimitive,
|
||||
MessagePrimitive,
|
||||
useAuiState,
|
||||
useAui,
|
||||
} from "@assistant-ui/react";
|
||||
import { useShallow } from "zustand/shallow";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const useFileSrc = (file: File | undefined) => {
|
||||
const [src, setSrc] = useState<string | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (!file) {
|
||||
setSrc(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const objectUrl = URL.createObjectURL(file);
|
||||
setSrc(objectUrl);
|
||||
|
||||
return () => {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
};
|
||||
}, [file]);
|
||||
|
||||
return src;
|
||||
};
|
||||
|
||||
const useAttachmentSrc = () => {
|
||||
const { file, src } = useAuiState(
|
||||
useShallow((s): { file?: File; src?: string } => {
|
||||
if (s.attachment.type !== "image") return {};
|
||||
if (s.attachment.file) return { file: s.attachment.file };
|
||||
const src = s.attachment.content?.filter((c) => c.type === "image")[0]
|
||||
?.image;
|
||||
if (!src) return {};
|
||||
return { src };
|
||||
}),
|
||||
);
|
||||
|
||||
return useFileSrc(file) ?? src;
|
||||
};
|
||||
|
||||
type AttachmentPreviewProps = {
|
||||
src: string;
|
||||
};
|
||||
|
||||
const AttachmentPreview: FC<AttachmentPreviewProps> = ({ src }) => {
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
alt="Image Preview"
|
||||
className={cn(
|
||||
"block h-auto max-h-[80vh] w-auto max-w-full object-contain",
|
||||
isLoaded
|
||||
? "aui-attachment-preview-image-loaded"
|
||||
: "aui-attachment-preview-image-loading invisible",
|
||||
)}
|
||||
onLoad={() => setIsLoaded(true)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const AttachmentPreviewDialog: FC<PropsWithChildren> = ({ children }) => {
|
||||
const src = useAttachmentSrc();
|
||||
|
||||
if (!src) return children;
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger
|
||||
className="aui-attachment-preview-trigger cursor-pointer transition-colors hover:bg-accent/50"
|
||||
asChild
|
||||
>
|
||||
{children}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="aui-attachment-preview-dialog-content p-2 sm:max-w-3xl [&>button]:rounded-full [&>button]:bg-foreground/60 [&>button]:p-1 [&>button]:opacity-100 [&>button]:ring-0! [&_svg]:text-background [&>button]:hover:[&_svg]:text-destructive">
|
||||
<DialogTitle className="aui-sr-only sr-only">
|
||||
Image Attachment Preview
|
||||
</DialogTitle>
|
||||
<div className="aui-attachment-preview relative mx-auto flex max-h-[80dvh] w-full items-center justify-center overflow-hidden bg-background">
|
||||
<AttachmentPreview src={src} />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const AttachmentThumb: FC = () => {
|
||||
const src = useAttachmentSrc();
|
||||
|
||||
return (
|
||||
<Avatar className="aui-attachment-tile-avatar h-full w-full rounded-none">
|
||||
<AvatarImage
|
||||
src={src}
|
||||
alt="Attachment preview"
|
||||
className="aui-attachment-tile-image object-cover"
|
||||
/>
|
||||
<AvatarFallback>
|
||||
<FileText className="aui-attachment-tile-fallback-icon size-8 text-muted-foreground" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
);
|
||||
};
|
||||
|
||||
const AttachmentUI: FC = () => {
|
||||
const aui = useAui();
|
||||
const isComposer = aui.attachment.source !== "message";
|
||||
|
||||
const isImage = useAuiState((s) => s.attachment.type === "image");
|
||||
const typeLabel = useAuiState((s) => {
|
||||
const type = s.attachment.type;
|
||||
switch (type) {
|
||||
case "image":
|
||||
return "Image";
|
||||
case "document":
|
||||
return "Document";
|
||||
case "file":
|
||||
return "File";
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<AttachmentPrimitive.Root
|
||||
className={cn(
|
||||
"aui-attachment-root relative",
|
||||
isImage && "aui-attachment-root-composer only:*:first:size-24",
|
||||
)}
|
||||
>
|
||||
<AttachmentPreviewDialog>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className="aui-attachment-tile size-14 cursor-pointer overflow-hidden rounded-[calc(var(--composer-radius)-var(--composer-padding))] border bg-muted transition-opacity hover:opacity-75"
|
||||
role="button"
|
||||
aria-label={`${typeLabel} attachment`}
|
||||
>
|
||||
<AttachmentThumb />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
</AttachmentPreviewDialog>
|
||||
{isComposer && <AttachmentRemove />}
|
||||
</AttachmentPrimitive.Root>
|
||||
<TooltipContent side="top">
|
||||
<AttachmentPrimitive.Name />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const AttachmentRemove: FC = () => {
|
||||
return (
|
||||
<AttachmentPrimitive.Remove asChild>
|
||||
<TooltipIconButton
|
||||
tooltip="Remove file"
|
||||
className="aui-attachment-tile-remove absolute top-1.5 right-1.5 size-3.5 rounded-full bg-white text-muted-foreground opacity-100 shadow-sm hover:bg-white! [&_svg]:text-black hover:[&_svg]:text-destructive"
|
||||
side="top"
|
||||
>
|
||||
<XIcon className="aui-attachment-remove-icon size-3 dark:stroke-[2.5px]" />
|
||||
</TooltipIconButton>
|
||||
</AttachmentPrimitive.Remove>
|
||||
);
|
||||
};
|
||||
|
||||
export const UserMessageAttachments: FC = () => {
|
||||
return (
|
||||
<div className="aui-user-message-attachments-end col-span-full col-start-1 row-start-1 flex w-full flex-row justify-end gap-2">
|
||||
<MessagePrimitive.Attachments>
|
||||
{() => <AttachmentUI />}
|
||||
</MessagePrimitive.Attachments>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ComposerAttachments: FC = () => {
|
||||
return (
|
||||
<div className="aui-composer-attachments flex w-full flex-row items-center gap-2 overflow-x-auto empty:hidden">
|
||||
<ComposerPrimitive.Attachments>
|
||||
{() => <AttachmentUI />}
|
||||
</ComposerPrimitive.Attachments>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ComposerAddAttachment: FC = () => {
|
||||
return (
|
||||
<ComposerPrimitive.AddAttachment asChild>
|
||||
<TooltipIconButton
|
||||
tooltip="Add Attachment"
|
||||
side="bottom"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="aui-composer-add-attachment size-8 rounded-full p-1 font-semibold text-xs hover:bg-muted-foreground/15 dark:border-muted-foreground/15 dark:hover:bg-muted-foreground/30"
|
||||
aria-label="Add Attachment"
|
||||
>
|
||||
<PlusIcon className="aui-attachment-add-icon size-5 stroke-[1.5px]" />
|
||||
</TooltipIconButton>
|
||||
</ComposerPrimitive.AddAttachment>
|
||||
);
|
||||
};
|
||||
62
frontend/components/assistant-ui/composer.tsx
Normal file
62
frontend/components/assistant-ui/composer.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import { type FC } from "react";
|
||||
import { ComposerPrimitive, AuiIf } from "@assistant-ui/react";
|
||||
import { ArrowUpIcon, SquareIcon } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { TooltipIconButton } from "./tooltip-icon-button";
|
||||
|
||||
const ComposerAction: FC = () => {
|
||||
return (
|
||||
<div className="relative flex items-center justify-end">
|
||||
<AuiIf condition={(s) => !s.thread.isRunning}>
|
||||
<ComposerPrimitive.Send asChild>
|
||||
<TooltipIconButton
|
||||
tooltip="Odoslať"
|
||||
side="bottom"
|
||||
type="button"
|
||||
variant="default"
|
||||
size="icon"
|
||||
className="size-8 rounded-full bg-blue-600 hover:bg-blue-700 border-0"
|
||||
aria-label="Send message"
|
||||
>
|
||||
<ArrowUpIcon className="size-4" />
|
||||
</TooltipIconButton>
|
||||
</ComposerPrimitive.Send>
|
||||
</AuiIf>
|
||||
<AuiIf condition={(s) => s.thread.isRunning}>
|
||||
<ComposerPrimitive.Cancel asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="default"
|
||||
size="icon"
|
||||
className="size-8 rounded-full bg-blue-600 hover:bg-blue-700 border-0"
|
||||
aria-label="Stop generating"
|
||||
>
|
||||
<SquareIcon className="size-3 fill-current" />
|
||||
</Button>
|
||||
</ComposerPrimitive.Cancel>
|
||||
</AuiIf>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ThreadComposer: FC = () => {
|
||||
return (
|
||||
<ComposerPrimitive.Root className="relative flex w-full flex-col">
|
||||
<div
|
||||
data-slot="composer-shell"
|
||||
className="flex w-full flex-col gap-2 rounded-2xl border border-blue-500/20 bg-card p-3 transition-shadow focus-within:border-blue-500/50 focus-within:ring-2 focus-within:ring-blue-500/10"
|
||||
>
|
||||
<ComposerPrimitive.Input
|
||||
placeholder="Napíšte správu..."
|
||||
className="max-h-32 min-h-10 w-full resize-none bg-transparent px-2 py-1 text-sm outline-none placeholder:text-muted-foreground/60"
|
||||
rows={1}
|
||||
autoFocus
|
||||
aria-label="Message input"
|
||||
/>
|
||||
<ComposerAction />
|
||||
</div>
|
||||
</ComposerPrimitive.Root>
|
||||
);
|
||||
};
|
||||
25
frontend/components/assistant-ui/header.tsx
Normal file
25
frontend/components/assistant-ui/header.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { type FC } from "react";
|
||||
import { SquarePenIcon, ChevronDownIcon, ScaleIcon } from "lucide-react";
|
||||
|
||||
export const ThreadHeader: FC = () => {
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-4 py-3 border-b border-border/40">
|
||||
<button className="p-1.5 rounded-md hover:bg-accent transition-colors">
|
||||
<SquarePenIcon className="size-5 text-muted-foreground" />
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg hover:bg-accent cursor-pointer transition-colors">
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
Legal AI Assistant
|
||||
</span>
|
||||
<ChevronDownIcon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<div className="ml-auto p-1.5">
|
||||
<ScaleIcon className="size-5 text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
160
frontend/components/assistant-ui/messages.tsx
Normal file
160
frontend/components/assistant-ui/messages.tsx
Normal file
@ -0,0 +1,160 @@
|
||||
"use client";
|
||||
|
||||
import { type FC } from "react";
|
||||
import {
|
||||
ActionBarMorePrimitive,
|
||||
ActionBarPrimitive,
|
||||
AuiIf,
|
||||
BranchPickerPrimitive,
|
||||
ErrorPrimitive,
|
||||
MessagePrimitive,
|
||||
useAuiState,
|
||||
} from "@assistant-ui/react";
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
CopyIcon,
|
||||
DownloadIcon,
|
||||
MoreHorizontalIcon,
|
||||
RefreshCwIcon,
|
||||
ScaleIcon,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { MarkdownText } from "./markdown-text";
|
||||
import { ToolFallback } from "./tool-fallback";
|
||||
import { TooltipIconButton } from "./tooltip-icon-button";
|
||||
|
||||
const MessageError: FC = () => {
|
||||
return (
|
||||
<MessagePrimitive.Error>
|
||||
<ErrorPrimitive.Root className="mt-2 rounded-md border border-destructive bg-destructive/10 p-3 text-destructive text-sm">
|
||||
<ErrorPrimitive.Message className="line-clamp-2" />
|
||||
</ErrorPrimitive.Root>
|
||||
</MessagePrimitive.Error>
|
||||
);
|
||||
};
|
||||
|
||||
const AssistantActionBar: FC = () => {
|
||||
return (
|
||||
<ActionBarPrimitive.Root
|
||||
hideWhenRunning
|
||||
autohide="not-last"
|
||||
autohideFloat="single-branch"
|
||||
className="col-start-3 row-start-2 -ml-1 flex gap-1 text-muted-foreground"
|
||||
>
|
||||
<ActionBarPrimitive.Copy asChild>
|
||||
<TooltipIconButton tooltip="Kopírovať">
|
||||
<AuiIf condition={(s) => s.message.isCopied}>
|
||||
<CheckIcon />
|
||||
</AuiIf>
|
||||
<AuiIf condition={(s) => !s.message.isCopied}>
|
||||
<CopyIcon />
|
||||
</AuiIf>
|
||||
</TooltipIconButton>
|
||||
</ActionBarPrimitive.Copy>
|
||||
<ActionBarPrimitive.Reload asChild>
|
||||
<TooltipIconButton tooltip="Obnoviť">
|
||||
<RefreshCwIcon />
|
||||
</TooltipIconButton>
|
||||
</ActionBarPrimitive.Reload>
|
||||
<ActionBarMorePrimitive.Root>
|
||||
<ActionBarMorePrimitive.Trigger asChild>
|
||||
<TooltipIconButton tooltip="Viac" className="data-[state=open]:bg-accent">
|
||||
<MoreHorizontalIcon />
|
||||
</TooltipIconButton>
|
||||
</ActionBarMorePrimitive.Trigger>
|
||||
<ActionBarMorePrimitive.Content
|
||||
side="bottom"
|
||||
align="start"
|
||||
className="z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md"
|
||||
>
|
||||
<ActionBarPrimitive.ExportMarkdown asChild>
|
||||
<ActionBarMorePrimitive.Item className="flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent">
|
||||
<DownloadIcon className="size-4" />
|
||||
Export ako Markdown
|
||||
</ActionBarMorePrimitive.Item>
|
||||
</ActionBarPrimitive.ExportMarkdown>
|
||||
</ActionBarMorePrimitive.Content>
|
||||
</ActionBarMorePrimitive.Root>
|
||||
</ActionBarPrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export const BranchPicker: FC<BranchPickerPrimitive.Root.Props> = ({
|
||||
className,
|
||||
...rest
|
||||
}) => {
|
||||
return (
|
||||
<BranchPickerPrimitive.Root
|
||||
hideWhenSingleBranch
|
||||
className={cn(
|
||||
"mr-2 -ml-2 inline-flex items-center text-muted-foreground text-xs",
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<BranchPickerPrimitive.Previous asChild>
|
||||
<TooltipIconButton tooltip="Predchádzajúci">
|
||||
<ChevronLeftIcon />
|
||||
</TooltipIconButton>
|
||||
</BranchPickerPrimitive.Previous>
|
||||
<span className="font-medium">
|
||||
<BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />
|
||||
</span>
|
||||
<BranchPickerPrimitive.Next asChild>
|
||||
<TooltipIconButton tooltip="Ďalší">
|
||||
<ChevronRightIcon />
|
||||
</TooltipIconButton>
|
||||
</BranchPickerPrimitive.Next>
|
||||
</BranchPickerPrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export const AssistantMessage: FC = () => {
|
||||
return (
|
||||
<MessagePrimitive.Root
|
||||
className="fade-in slide-in-from-bottom-1 relative mx-auto w-full max-w-(--thread-max-width) animate-in py-3 duration-150"
|
||||
data-role="assistant"
|
||||
>
|
||||
<div className="flex gap-3 px-2">
|
||||
<div className="size-7 rounded-full bg-blue-600/20 border border-blue-500/30 flex items-center justify-center shrink-0 mt-0.5">
|
||||
<ScaleIcon className="size-3.5 text-blue-400" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-foreground leading-relaxed">
|
||||
<MessagePrimitive.Parts>
|
||||
{({ part }) => {
|
||||
if (part.type === "text") return <MarkdownText />;
|
||||
if (part.type === "tool-call")
|
||||
return part.toolUI ?? <ToolFallback {...part} />;
|
||||
return null;
|
||||
}}
|
||||
</MessagePrimitive.Parts>
|
||||
<MessageError />
|
||||
</div>
|
||||
<div className="mt-1 flex">
|
||||
<BranchPicker />
|
||||
<AssistantActionBar />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MessagePrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export const UserMessage: FC = () => {
|
||||
return (
|
||||
<MessagePrimitive.Root
|
||||
className="fade-in slide-in-from-bottom-1 mx-auto grid w-full max-w-(--thread-max-width) animate-in auto-rows-auto grid-cols-[minmax(72px,1fr)_auto] content-start gap-y-2 px-2 py-3 duration-150 [&:where(>*)]:col-start-2"
|
||||
data-role="user"
|
||||
>
|
||||
<div className="relative col-start-2 min-w-0">
|
||||
<div className="wrap-break-word rounded-2xl bg-blue-600/20 border border-blue-500/20 px-4 py-2.5 text-foreground empty:hidden">
|
||||
<MessagePrimitive.Parts />
|
||||
</div>
|
||||
</div>
|
||||
<BranchPicker className="col-span-full col-start-1 row-start-3 -mr-1 justify-end" />
|
||||
</MessagePrimitive.Root>
|
||||
);
|
||||
};
|
||||
@ -1,75 +1,17 @@
|
||||
import {
|
||||
ComposerAddAttachment,
|
||||
ComposerAttachments,
|
||||
UserMessageAttachments,
|
||||
} from "@/components/assistant-ui/attachment";
|
||||
import { MarkdownText } from "@/components/assistant-ui/markdown-text";
|
||||
import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
|
||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
ActionBarMorePrimitive,
|
||||
ActionBarPrimitive,
|
||||
AuiIf,
|
||||
BranchPickerPrimitive,
|
||||
ComposerPrimitive,
|
||||
ErrorPrimitive,
|
||||
MessagePrimitive,
|
||||
SuggestionPrimitive,
|
||||
ThreadPrimitive,
|
||||
useAuiState,
|
||||
} from "@assistant-ui/react";
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
ArrowUpIcon,
|
||||
CheckIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
CopyIcon,
|
||||
DownloadIcon,
|
||||
MoreHorizontalIcon,
|
||||
PencilIcon,
|
||||
RefreshCwIcon,
|
||||
SquareIcon,
|
||||
} from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
"use client";
|
||||
|
||||
export const Thread: FC = () => {
|
||||
return (
|
||||
<ThreadPrimitive.Root
|
||||
className="aui-root aui-thread-root @container flex h-full flex-col bg-background"
|
||||
style={{
|
||||
["--thread-max-width" as string]: "44rem",
|
||||
["--composer-radius" as string]: "24px",
|
||||
["--composer-padding" as string]: "10px",
|
||||
}}
|
||||
>
|
||||
<ThreadPrimitive.Viewport
|
||||
turnAnchor="top"
|
||||
className="aui-thread-viewport relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll scroll-smooth px-4 pt-4"
|
||||
>
|
||||
<AuiIf condition={(s) => s.thread.isEmpty}>
|
||||
<ThreadWelcome />
|
||||
</AuiIf>
|
||||
import { type FC } from "react";
|
||||
import { AuiIf, ThreadPrimitive, useAuiState } from "@assistant-ui/react";
|
||||
import { ArrowDownIcon } from "lucide-react";
|
||||
|
||||
<ThreadPrimitive.Messages>
|
||||
{() => <ThreadMessage />}
|
||||
</ThreadPrimitive.Messages>
|
||||
|
||||
<ThreadPrimitive.ViewportFooter className="aui-thread-viewport-footer sticky bottom-0 mx-auto mt-auto flex w-full max-w-(--thread-max-width) flex-col gap-4 overflow-visible rounded-t-(--composer-radius) bg-background pb-4 md:pb-6">
|
||||
<ThreadScrollToBottom />
|
||||
<Composer />
|
||||
</ThreadPrimitive.ViewportFooter>
|
||||
</ThreadPrimitive.Viewport>
|
||||
</ThreadPrimitive.Root>
|
||||
);
|
||||
};
|
||||
import { ThreadHeader } from "./header";
|
||||
import { ThreadWelcome } from "./welcome";
|
||||
import { AssistantMessage, UserMessage } from "./messages";
|
||||
import { ThreadComposer } from "./composer";
|
||||
import { TooltipIconButton } from "./tooltip-icon-button";
|
||||
|
||||
const ThreadMessage: FC = () => {
|
||||
const role = useAuiState((s) => s.message.role);
|
||||
const isEditing = useAuiState((s) => s.message.composer.isEditing);
|
||||
if (isEditing) return <EditComposer />;
|
||||
if (role === "user") return <UserMessage />;
|
||||
return <AssistantMessage />;
|
||||
};
|
||||
@ -80,7 +22,7 @@ const ThreadScrollToBottom: FC = () => {
|
||||
<TooltipIconButton
|
||||
tooltip="Scroll to bottom"
|
||||
variant="outline"
|
||||
className="aui-thread-scroll-to-bottom absolute -top-12 z-10 self-center rounded-full p-4 disabled:invisible dark:border-border dark:bg-background dark:hover:bg-accent"
|
||||
className="absolute -top-12 z-10 self-center rounded-full p-4 disabled:invisible dark:border-border dark:bg-background dark:hover:bg-accent"
|
||||
>
|
||||
<ArrowDownIcon />
|
||||
</TooltipIconButton>
|
||||
@ -88,281 +30,38 @@ const ThreadScrollToBottom: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const ThreadWelcome: FC = () => {
|
||||
export const Thread: FC = () => {
|
||||
return (
|
||||
<div className="aui-thread-welcome-root mx-auto my-auto flex w-full max-w-(--thread-max-width) grow flex-col">
|
||||
<div className="aui-thread-welcome-center flex w-full grow flex-col items-center justify-center">
|
||||
<div className="aui-thread-welcome-message flex size-full flex-col justify-center px-4">
|
||||
<h1 className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in fill-mode-both font-semibold text-2xl duration-200">
|
||||
Hello there!
|
||||
</h1>
|
||||
<p className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in fill-mode-both text-muted-foreground text-xl delay-75 duration-200">
|
||||
How can I help you today?
|
||||
<ThreadPrimitive.Root
|
||||
className="aui-root aui-thread-root @container flex h-full flex-col bg-background"
|
||||
style={{
|
||||
["--thread-max-width" as string]: "48rem",
|
||||
["--composer-radius" as string]: "16px",
|
||||
["--composer-padding" as string]: "12px",
|
||||
}}
|
||||
>
|
||||
<ThreadHeader />
|
||||
|
||||
<ThreadPrimitive.Viewport
|
||||
turnAnchor="top"
|
||||
className="relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll scroll-smooth px-4 pt-4"
|
||||
>
|
||||
<AuiIf condition={(s) => s.thread.isEmpty}>
|
||||
<ThreadWelcome />
|
||||
</AuiIf>
|
||||
|
||||
<ThreadPrimitive.Messages>
|
||||
{() => <ThreadMessage />}
|
||||
</ThreadPrimitive.Messages>
|
||||
|
||||
<ThreadPrimitive.ViewportFooter className="sticky bottom-0 mx-auto mt-auto flex w-full max-w-(--thread-max-width) flex-col gap-4 overflow-visible rounded-t-2xl bg-background pb-4 md:pb-6 pt-2">
|
||||
<ThreadScrollToBottom />
|
||||
<ThreadComposer />
|
||||
<p className="text-center text-xs text-muted-foreground/50 pb-1">
|
||||
Právny AI Asistent môže robiť chyby. Overte dôležité informácie.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ThreadSuggestions />
|
||||
</div>
|
||||
</ThreadPrimitive.ViewportFooter>
|
||||
</ThreadPrimitive.Viewport>
|
||||
</ThreadPrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
const ThreadSuggestions: FC = () => {
|
||||
return (
|
||||
<div className="aui-thread-welcome-suggestions grid w-full @md:grid-cols-2 gap-2 pb-4">
|
||||
<ThreadPrimitive.Suggestions>
|
||||
{() => <ThreadSuggestionItem />}
|
||||
</ThreadPrimitive.Suggestions>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ThreadSuggestionItem: FC = () => {
|
||||
return (
|
||||
<div className="aui-thread-welcome-suggestion-display fade-in slide-in-from-bottom-2 @md:nth-[n+3]:block nth-[n+3]:hidden animate-in fill-mode-both duration-200">
|
||||
<SuggestionPrimitive.Trigger send asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="aui-thread-welcome-suggestion h-auto w-full @md:flex-col flex-wrap items-start justify-start gap-1 rounded-3xl border bg-background px-4 py-3 text-left text-sm transition-colors hover:bg-muted"
|
||||
>
|
||||
<SuggestionPrimitive.Title className="aui-thread-welcome-suggestion-text-1 font-medium" />
|
||||
<SuggestionPrimitive.Description className="aui-thread-welcome-suggestion-text-2 text-muted-foreground empty:hidden" />
|
||||
</Button>
|
||||
</SuggestionPrimitive.Trigger>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Composer: FC = () => {
|
||||
return (
|
||||
<ComposerPrimitive.Root className="aui-composer-root relative flex w-full flex-col">
|
||||
<ComposerPrimitive.AttachmentDropzone asChild>
|
||||
<div
|
||||
data-slot="composer-shell"
|
||||
className="flex w-full flex-col gap-2 rounded-(--composer-radius) border bg-background p-(--composer-padding) transition-shadow focus-within:border-ring/75 focus-within:ring-2 focus-within:ring-ring/20 data-[dragging=true]:border-ring data-[dragging=true]:border-dashed data-[dragging=true]:bg-accent/50"
|
||||
>
|
||||
<ComposerAttachments />
|
||||
<ComposerPrimitive.Input
|
||||
placeholder="Send a message..."
|
||||
className="aui-composer-input max-h-32 min-h-10 w-full resize-none bg-transparent px-1.75 py-1 text-sm outline-none placeholder:text-muted-foreground/80"
|
||||
rows={1}
|
||||
autoFocus
|
||||
aria-label="Message input"
|
||||
/>
|
||||
<ComposerAction />
|
||||
</div>
|
||||
</ComposerPrimitive.AttachmentDropzone>
|
||||
</ComposerPrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
const ComposerAction: FC = () => {
|
||||
return (
|
||||
<div className="aui-composer-action-wrapper relative flex items-center justify-between">
|
||||
<ComposerAddAttachment />
|
||||
<AuiIf condition={(s) => !s.thread.isRunning}>
|
||||
<ComposerPrimitive.Send asChild>
|
||||
<TooltipIconButton
|
||||
tooltip="Send message"
|
||||
side="bottom"
|
||||
type="button"
|
||||
variant="default"
|
||||
size="icon"
|
||||
className="aui-composer-send size-8 rounded-full"
|
||||
aria-label="Send message"
|
||||
>
|
||||
<ArrowUpIcon className="aui-composer-send-icon size-4" />
|
||||
</TooltipIconButton>
|
||||
</ComposerPrimitive.Send>
|
||||
</AuiIf>
|
||||
<AuiIf condition={(s) => s.thread.isRunning}>
|
||||
<ComposerPrimitive.Cancel asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="default"
|
||||
size="icon"
|
||||
className="aui-composer-cancel size-8 rounded-full"
|
||||
aria-label="Stop generating"
|
||||
>
|
||||
<SquareIcon className="aui-composer-cancel-icon size-3 fill-current" />
|
||||
</Button>
|
||||
</ComposerPrimitive.Cancel>
|
||||
</AuiIf>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MessageError: FC = () => {
|
||||
return (
|
||||
<MessagePrimitive.Error>
|
||||
<ErrorPrimitive.Root className="aui-message-error-root mt-2 rounded-md border border-destructive bg-destructive/10 p-3 text-destructive text-sm dark:bg-destructive/5 dark:text-red-200">
|
||||
<ErrorPrimitive.Message className="aui-message-error-message line-clamp-2" />
|
||||
</ErrorPrimitive.Root>
|
||||
</MessagePrimitive.Error>
|
||||
);
|
||||
};
|
||||
|
||||
const AssistantMessage: FC = () => {
|
||||
return (
|
||||
<MessagePrimitive.Root
|
||||
className="aui-assistant-message-root fade-in slide-in-from-bottom-1 relative mx-auto w-full max-w-(--thread-max-width) animate-in py-3 duration-150"
|
||||
data-role="assistant"
|
||||
>
|
||||
<div className="aui-assistant-message-content wrap-break-word px-2 text-foreground leading-relaxed">
|
||||
<MessagePrimitive.Parts>
|
||||
{({ part }) => {
|
||||
if (part.type === "text") return <MarkdownText />;
|
||||
if (part.type === "tool-call")
|
||||
return part.toolUI ?? <ToolFallback {...part} />;
|
||||
return null;
|
||||
}}
|
||||
</MessagePrimitive.Parts>
|
||||
<MessageError />
|
||||
</div>
|
||||
|
||||
<div className="aui-assistant-message-footer mt-1 ml-2 flex">
|
||||
<BranchPicker />
|
||||
<AssistantActionBar />
|
||||
</div>
|
||||
</MessagePrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
const AssistantActionBar: FC = () => {
|
||||
return (
|
||||
<ActionBarPrimitive.Root
|
||||
hideWhenRunning
|
||||
autohide="not-last"
|
||||
autohideFloat="single-branch"
|
||||
className="aui-assistant-action-bar-root col-start-3 row-start-2 -ml-1 flex gap-1 text-muted-foreground data-floating:absolute data-floating:rounded-md data-floating:border data-floating:bg-background data-floating:p-1 data-floating:shadow-sm"
|
||||
>
|
||||
<ActionBarPrimitive.Copy asChild>
|
||||
<TooltipIconButton tooltip="Copy">
|
||||
<AuiIf condition={(s) => s.message.isCopied}>
|
||||
<CheckIcon />
|
||||
</AuiIf>
|
||||
<AuiIf condition={(s) => !s.message.isCopied}>
|
||||
<CopyIcon />
|
||||
</AuiIf>
|
||||
</TooltipIconButton>
|
||||
</ActionBarPrimitive.Copy>
|
||||
<ActionBarPrimitive.Reload asChild>
|
||||
<TooltipIconButton tooltip="Refresh">
|
||||
<RefreshCwIcon />
|
||||
</TooltipIconButton>
|
||||
</ActionBarPrimitive.Reload>
|
||||
<ActionBarMorePrimitive.Root>
|
||||
<ActionBarMorePrimitive.Trigger asChild>
|
||||
<TooltipIconButton
|
||||
tooltip="More"
|
||||
className="data-[state=open]:bg-accent"
|
||||
>
|
||||
<MoreHorizontalIcon />
|
||||
</TooltipIconButton>
|
||||
</ActionBarMorePrimitive.Trigger>
|
||||
<ActionBarMorePrimitive.Content
|
||||
side="bottom"
|
||||
align="start"
|
||||
className="aui-action-bar-more-content z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md"
|
||||
>
|
||||
<ActionBarPrimitive.ExportMarkdown asChild>
|
||||
<ActionBarMorePrimitive.Item className="aui-action-bar-more-item flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground">
|
||||
<DownloadIcon className="size-4" />
|
||||
Export as Markdown
|
||||
</ActionBarMorePrimitive.Item>
|
||||
</ActionBarPrimitive.ExportMarkdown>
|
||||
</ActionBarMorePrimitive.Content>
|
||||
</ActionBarMorePrimitive.Root>
|
||||
</ActionBarPrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
const UserMessage: FC = () => {
|
||||
return (
|
||||
<MessagePrimitive.Root
|
||||
className="aui-user-message-root fade-in slide-in-from-bottom-1 mx-auto grid w-full max-w-(--thread-max-width) animate-in auto-rows-auto grid-cols-[minmax(72px,1fr)_auto] content-start gap-y-2 px-2 py-3 duration-150 [&:where(>*)]:col-start-2"
|
||||
data-role="user"
|
||||
>
|
||||
<UserMessageAttachments />
|
||||
|
||||
<div className="aui-user-message-content-wrapper relative col-start-2 min-w-0">
|
||||
<div className="aui-user-message-content wrap-break-word peer rounded-2xl bg-muted px-4 py-2.5 text-foreground empty:hidden">
|
||||
<MessagePrimitive.Parts />
|
||||
</div>
|
||||
<div className="aui-user-action-bar-wrapper absolute top-1/2 left-0 -translate-x-full -translate-y-1/2 pr-2 peer-empty:hidden">
|
||||
<UserActionBar />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BranchPicker className="aui-user-branch-picker col-span-full col-start-1 row-start-3 -mr-1 justify-end" />
|
||||
</MessagePrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
const UserActionBar: FC = () => {
|
||||
return (
|
||||
<ActionBarPrimitive.Root
|
||||
hideWhenRunning
|
||||
autohide="not-last"
|
||||
className="aui-user-action-bar-root flex flex-col items-end"
|
||||
>
|
||||
<ActionBarPrimitive.Edit asChild>
|
||||
<TooltipIconButton tooltip="Edit" className="aui-user-action-edit p-4">
|
||||
<PencilIcon />
|
||||
</TooltipIconButton>
|
||||
</ActionBarPrimitive.Edit>
|
||||
</ActionBarPrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
const EditComposer: FC = () => {
|
||||
return (
|
||||
<MessagePrimitive.Root className="aui-edit-composer-wrapper mx-auto flex w-full max-w-(--thread-max-width) flex-col px-2 py-3">
|
||||
<ComposerPrimitive.Root className="aui-edit-composer-root ml-auto flex w-full max-w-[85%] flex-col rounded-2xl bg-muted">
|
||||
<ComposerPrimitive.Input
|
||||
className="aui-edit-composer-input min-h-14 w-full resize-none bg-transparent p-4 text-foreground text-sm outline-none"
|
||||
autoFocus
|
||||
/>
|
||||
<div className="aui-edit-composer-footer mx-3 mb-3 flex items-center gap-2 self-end">
|
||||
<ComposerPrimitive.Cancel asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
</ComposerPrimitive.Cancel>
|
||||
<ComposerPrimitive.Send asChild>
|
||||
<Button size="sm">Update</Button>
|
||||
</ComposerPrimitive.Send>
|
||||
</div>
|
||||
</ComposerPrimitive.Root>
|
||||
</MessagePrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
const BranchPicker: FC<BranchPickerPrimitive.Root.Props> = ({
|
||||
className,
|
||||
...rest
|
||||
}) => {
|
||||
return (
|
||||
<BranchPickerPrimitive.Root
|
||||
hideWhenSingleBranch
|
||||
className={cn(
|
||||
"aui-branch-picker-root mr-2 -ml-2 inline-flex items-center text-muted-foreground text-xs",
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<BranchPickerPrimitive.Previous asChild>
|
||||
<TooltipIconButton tooltip="Previous">
|
||||
<ChevronLeftIcon />
|
||||
</TooltipIconButton>
|
||||
</BranchPickerPrimitive.Previous>
|
||||
<span className="aui-branch-picker-state font-medium">
|
||||
<BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />
|
||||
</span>
|
||||
<BranchPickerPrimitive.Next asChild>
|
||||
<TooltipIconButton tooltip="Next">
|
||||
<ChevronRightIcon />
|
||||
</TooltipIconButton>
|
||||
</BranchPickerPrimitive.Next>
|
||||
</BranchPickerPrimitive.Root>
|
||||
);
|
||||
};
|
||||
};
|
||||
90
frontend/components/assistant-ui/welcome.tsx
Normal file
90
frontend/components/assistant-ui/welcome.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
|
||||
import { type FC } from "react";
|
||||
|
||||
const SUGGESTIONS = [
|
||||
{
|
||||
icon: "🔍",
|
||||
title: "Aké právne dáta",
|
||||
description: "môže agent nájsť?",
|
||||
prompt: "Aké právne dáta môžeš vyhľadať?",
|
||||
},
|
||||
{
|
||||
icon: "🚫",
|
||||
title: "Čo agent nesmie",
|
||||
description: "robiť alebo používať?",
|
||||
prompt: "Čo nie si oprávnený robiť alebo použiť?",
|
||||
},
|
||||
{
|
||||
icon: "🤖",
|
||||
title: "Detaily AI modelu",
|
||||
description: "aký model používaš?",
|
||||
prompt: "Aké sú detaily tvojho AI modelu?",
|
||||
},
|
||||
{
|
||||
icon: "📊",
|
||||
title: "Zdroje dát",
|
||||
description: "odkiaľ čerpáš informácie?",
|
||||
prompt: "Aké dátové zdroje agent využíva?",
|
||||
},
|
||||
];
|
||||
|
||||
const ThreadSuggestions: FC = () => {
|
||||
const handleClick = (prompt: string) => {
|
||||
const input = document.querySelector(
|
||||
'textarea[aria-label="Message input"]'
|
||||
) as HTMLTextAreaElement;
|
||||
if (input) {
|
||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLTextAreaElement.prototype,
|
||||
"value"
|
||||
)?.set;
|
||||
nativeInputValueSetter?.call(input, prompt);
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
input.focus();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid w-full grid-cols-2 @lg:grid-cols-4 gap-2 pb-4 px-2">
|
||||
{SUGGESTIONS.map((s) => (
|
||||
<button
|
||||
key={s.title}
|
||||
onClick={() => handleClick(s.prompt)}
|
||||
className="flex flex-col items-start gap-1 rounded-xl border border-blue-500/20 bg-card px-4 py-3 text-left text-sm transition-colors hover:bg-blue-600/10 hover:border-blue-500/40 cursor-pointer"
|
||||
>
|
||||
<span className="font-medium text-foreground">
|
||||
{s.icon} {s.title}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-xs">{s.description}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ThreadWelcome: FC = () => {
|
||||
return (
|
||||
<div className="mx-auto my-auto flex w-full max-w-(--thread-max-width) grow flex-col items-center justify-center gap-6">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="size-24 rounded-full overflow-hidden ring-2 ring-blue-500/30 shadow-lg shadow-blue-500/20">
|
||||
<img
|
||||
src="/logo.png"
|
||||
alt="Legal AI"
|
||||
className="size-full object-cover"
|
||||
onError={(e) => {
|
||||
e.currentTarget.style.display = "none";
|
||||
e.currentTarget.parentElement!.innerHTML = `<div class="size-full bg-blue-600/20 flex items-center justify-center text-4xl">⚖️</div>`;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-1 text-center">
|
||||
<h1 className="fade-in slide-in-from-bottom-1 animate-in fill-mode-both font-semibold text-2xl duration-200">
|
||||
Legal AI Assistant
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<ThreadSuggestions />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,109 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Avatar as AvatarPrimitive } from "radix-ui";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Root> & {
|
||||
size?: "default" | "sm" | "lg";
|
||||
}) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
data-slot="avatar"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/avatar relative flex size-8 shrink-0 select-none overflow-hidden rounded-full data-[size=lg]:size-10 data-[size=sm]:size-6",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarImage({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
className={cn("aspect-square size-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"flex size-full items-center justify-center rounded-full bg-muted text-muted-foreground text-sm group-data-[size=sm]/avatar:text-xs",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="avatar-badge"
|
||||
className={cn(
|
||||
"absolute right-0 bottom-0 z-10 inline-flex select-none items-center justify-center rounded-full bg-primary text-primary-foreground ring-2 ring-background",
|
||||
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
|
||||
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
|
||||
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="avatar-group"
|
||||
className={cn(
|
||||
"group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarGroupCount({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="avatar-group-count"
|
||||
className={cn(
|
||||
"relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground text-sm ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Avatar,
|
||||
AvatarImage,
|
||||
AvatarFallback,
|
||||
AvatarBadge,
|
||||
AvatarGroup,
|
||||
AvatarGroupCount,
|
||||
};
|
||||
@ -1,158 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { XIcon } from "lucide-react";
|
||||
import { Dialog as DialogPrimitive } from "radix-ui";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg outline-none duration-200 data-[state=closed]:animate-out data-[state=open]:animate-in sm:max-w-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogFooter({
|
||||
className,
|
||||
showCloseButton = false,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("font-semibold text-lg leading-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
};
|
||||
@ -1,7 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
devIndicators: false,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
6
frontend/package-lock.json
generated
6
frontend/package-lock.json
generated
@ -5340,9 +5340,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eventsource-parser": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.7.tgz",
|
||||
"integrity": "sha512-zwxwiQqexizSXFZV13zMiEtW1E3lv7RlUv+1f5FBiR4x7wFhEjm3aFTyYkZQWzyN08WnPdox015GoRH5D/E5YA==",
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz",
|
||||
"integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
|
||||
BIN
frontend/public/logo.png
Normal file
BIN
frontend/public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 286 KiB |
Loading…
Reference in New Issue
Block a user