changed FRONTEND and docker configs
This commit is contained in:
parent
1c90b3d75f
commit
f2123a5779
@ -1,19 +1,50 @@
|
|||||||
|
# Git
|
||||||
.git
|
.git
|
||||||
.gitignore
|
.gitignore
|
||||||
.venv
|
|
||||||
venv
|
# Python
|
||||||
.env
|
|
||||||
env/
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.py[cod]
|
||||||
*.pyo
|
*.pyo
|
||||||
*.pyd
|
*.pyd
|
||||||
.Python
|
.Python
|
||||||
|
*.so
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Virtual envs
|
||||||
|
.venv
|
||||||
|
venv
|
||||||
|
env/
|
||||||
|
|
||||||
|
# Env files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Editors
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
tests/
|
||||||
test/
|
test/
|
||||||
README.md
|
|
||||||
docker-compose.yml
|
# Node
|
||||||
|
node_modules/
|
||||||
|
frontend/node_modules/
|
||||||
|
.next/
|
||||||
|
frontend/.next/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Docker
|
||||||
Dockerfile
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
.dockerignore
|
.dockerignore
|
||||||
|
|
||||||
|
|
||||||
|
.files/
|
||||||
.chainlit/translations/*
|
.chainlit/translations/*
|
||||||
!.chainlit/translations/en-US.json
|
!.chainlit/translations/en-US.json
|
||||||
.files/
|
|
||||||
0
backend/__init__.py
Normal file
0
backend/__init__.py
Normal file
@ -12,4 +12,4 @@ COPY backend/ ./backend/
|
|||||||
|
|
||||||
EXPOSE 8001
|
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"
|
name: "legal-ai-assistant"
|
||||||
services:
|
services:
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
depends_on:
|
||||||
|
backend:
|
||||||
|
condition: service_started
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: backend/Dockerfile
|
dockerfile: /backend/Dockerfile
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
@ -26,18 +39,18 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "4000:4000"
|
- "4000:4000"
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- backend/.env
|
||||||
environment:
|
environment:
|
||||||
- GROQ_API_KEY=${GROQ_API_KEY}
|
- GROQ_API_KEY=${GROQ_API_KEY}
|
||||||
- GEMINI_API_KEY=${GEMINI_API_KEY}
|
- GEMINI_API_KEY=${GEMINI_API_KEY}
|
||||||
volumes:
|
volumes:
|
||||||
- ./config.yaml:/app/config.yaml:ro
|
- ./backend/config.yaml:/app/config.yaml:ro
|
||||||
command: ['--config', '/app/config.yaml', '--port', '4000']
|
command: ['--config', '/app/config.yaml', '--port', '4000']
|
||||||
|
|
||||||
mcp:
|
mcp:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: backend/mcp_server/Dockerfile
|
dockerfile: /backend/mcp_server/Dockerfile
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "8001:8001"
|
- "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) {
|
export async function POST(req: Request) {
|
||||||
const {
|
const body = await req.json();
|
||||||
messages,
|
|
||||||
system,
|
|
||||||
tools,
|
|
||||||
}: {
|
|
||||||
messages: UIMessage[];
|
|
||||||
system?: string;
|
|
||||||
tools?: Record<string, { description?: string; parameters: JSONSchema7 }>;
|
|
||||||
} = await req.json();
|
|
||||||
|
|
||||||
const result = streamText({
|
const response = await fetch("http://backend:8000/api/chat", {
|
||||||
model: openai("gpt-5-nano"),
|
method: "POST",
|
||||||
messages: await convertToModelMessages(messages),
|
headers: {
|
||||||
system,
|
"Content-Type": "application/json",
|
||||||
tools: {
|
|
||||||
...frontendTools(tools ?? {}),
|
|
||||||
},
|
},
|
||||||
|
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 {
|
:root {
|
||||||
--radius: 0.625rem;
|
--radius: 0.625rem;
|
||||||
--background: oklch(1 0 0);
|
--background: oklch(0.12 0.04 240);
|
||||||
--foreground: oklch(0.141 0.005 285.823);
|
--foreground: oklch(0.95 0.01 240);
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(0.17 0.05 240);
|
||||||
--card-foreground: oklch(0.141 0.005 285.823);
|
--card-foreground: oklch(0.95 0.01 240);
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(0.17 0.05 240);
|
||||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
--popover-foreground: oklch(0.95 0.01 240);
|
||||||
--primary: oklch(0.21 0.006 285.885);
|
--primary: oklch(0.6 0.18 240);
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
--primary-foreground: oklch(0.98 0 0);
|
||||||
--secondary: oklch(0.967 0.001 286.375);
|
--secondary: oklch(0.2 0.05 240);
|
||||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
--secondary-foreground: oklch(0.95 0.01 240);
|
||||||
--muted: oklch(0.967 0.001 286.375);
|
--muted: oklch(0.2 0.05 240);
|
||||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
--muted-foreground: oklch(0.65 0.05 240);
|
||||||
--accent: oklch(0.967 0.001 286.375);
|
--accent: oklch(0.22 0.06 240);
|
||||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
--accent-foreground: oklch(0.95 0.01 240);
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
--border: oklch(0.92 0.004 286.32);
|
--border: oklch(1 0 0 / 8%);
|
||||||
--input: oklch(0.92 0.004 286.32);
|
--input: oklch(1 0 0 / 12%);
|
||||||
--ring: oklch(0.705 0.015 286.067);
|
--ring: oklch(0.5 0.15 240);
|
||||||
--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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: oklch(0.141 0.005 285.823);
|
--background: oklch(0.12 0.04 240);
|
||||||
--foreground: oklch(0.985 0 0);
|
--foreground: oklch(0.95 0.01 240);
|
||||||
--card: oklch(0.21 0.006 285.885);
|
--card: oklch(0.17 0.05 240);
|
||||||
--card-foreground: oklch(0.985 0 0);
|
--card-foreground: oklch(0.95 0.01 240);
|
||||||
--popover: oklch(0.21 0.006 285.885);
|
--popover: oklch(0.17 0.05 240);
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
--popover-foreground: oklch(0.95 0.01 240);
|
||||||
--primary: oklch(0.92 0.004 286.32);
|
--primary: oklch(0.6 0.18 240);
|
||||||
--primary-foreground: oklch(0.21 0.006 285.885);
|
--primary-foreground: oklch(0.98 0 0);
|
||||||
--secondary: oklch(0.274 0.006 286.033);
|
--secondary: oklch(0.2 0.05 240);
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
--secondary-foreground: oklch(0.95 0.01 240);
|
||||||
--muted: oklch(0.274 0.006 286.033);
|
--muted: oklch(0.2 0.05 240);
|
||||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
--muted-foreground: oklch(0.65 0.05 240);
|
||||||
--accent: oklch(0.274 0.006 286.033);
|
--accent: oklch(0.22 0.06 240);
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
--accent-foreground: oklch(0.95 0.01 240);
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
--border: oklch(1 0 0 / 10%);
|
--border: oklch(1 0 0 / 8%);
|
||||||
--input: oklch(1 0 0 / 15%);
|
--input: oklch(1 0 0 / 12%);
|
||||||
--ring: oklch(0.552 0.016 285.938);
|
--ring: oklch(0.5 0.15 240);
|
||||||
--chart-1: oklch(0.488 0.243 264.376);
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
--chart-2: oklch(0.696 0.17 162.48);
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
--chart-3: oklch(0.769 0.188 70.08);
|
--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 {
|
"use client";
|
||||||
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";
|
|
||||||
|
|
||||||
export const Thread: FC = () => {
|
import { type FC } from "react";
|
||||||
return (
|
import { AuiIf, ThreadPrimitive, useAuiState } from "@assistant-ui/react";
|
||||||
<ThreadPrimitive.Root
|
import { ArrowDownIcon } from "lucide-react";
|
||||||
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>
|
|
||||||
|
|
||||||
<ThreadPrimitive.Messages>
|
import { ThreadHeader } from "./header";
|
||||||
{() => <ThreadMessage />}
|
import { ThreadWelcome } from "./welcome";
|
||||||
</ThreadPrimitive.Messages>
|
import { AssistantMessage, UserMessage } from "./messages";
|
||||||
|
import { ThreadComposer } from "./composer";
|
||||||
<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">
|
import { TooltipIconButton } from "./tooltip-icon-button";
|
||||||
<ThreadScrollToBottom />
|
|
||||||
<Composer />
|
|
||||||
</ThreadPrimitive.ViewportFooter>
|
|
||||||
</ThreadPrimitive.Viewport>
|
|
||||||
</ThreadPrimitive.Root>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ThreadMessage: FC = () => {
|
const ThreadMessage: FC = () => {
|
||||||
const role = useAuiState((s) => s.message.role);
|
const role = useAuiState((s) => s.message.role);
|
||||||
const isEditing = useAuiState((s) => s.message.composer.isEditing);
|
|
||||||
if (isEditing) return <EditComposer />;
|
|
||||||
if (role === "user") return <UserMessage />;
|
if (role === "user") return <UserMessage />;
|
||||||
return <AssistantMessage />;
|
return <AssistantMessage />;
|
||||||
};
|
};
|
||||||
@ -80,7 +22,7 @@ const ThreadScrollToBottom: FC = () => {
|
|||||||
<TooltipIconButton
|
<TooltipIconButton
|
||||||
tooltip="Scroll to bottom"
|
tooltip="Scroll to bottom"
|
||||||
variant="outline"
|
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 />
|
<ArrowDownIcon />
|
||||||
</TooltipIconButton>
|
</TooltipIconButton>
|
||||||
@ -88,281 +30,38 @@ const ThreadScrollToBottom: FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ThreadWelcome: FC = () => {
|
export const Thread: FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className="aui-thread-welcome-root mx-auto my-auto flex w-full max-w-(--thread-max-width) grow flex-col">
|
<ThreadPrimitive.Root
|
||||||
<div className="aui-thread-welcome-center flex w-full grow flex-col items-center justify-center">
|
className="aui-root aui-thread-root @container flex h-full flex-col bg-background"
|
||||||
<div className="aui-thread-welcome-message flex size-full flex-col justify-center px-4">
|
style={{
|
||||||
<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">
|
["--thread-max-width" as string]: "48rem",
|
||||||
Hello there!
|
["--composer-radius" as string]: "16px",
|
||||||
</h1>
|
["--composer-padding" as string]: "12px",
|
||||||
<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?
|
>
|
||||||
|
<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>
|
</p>
|
||||||
</div>
|
</ThreadPrimitive.ViewportFooter>
|
||||||
</div>
|
</ThreadPrimitive.Viewport>
|
||||||
<ThreadSuggestions />
|
</ThreadPrimitive.Root>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
devIndicators: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
6
frontend/package-lock.json
generated
6
frontend/package-lock.json
generated
@ -5340,9 +5340,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eventsource-parser": {
|
"node_modules/eventsource-parser": {
|
||||||
"version": "3.0.7",
|
"version": "3.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz",
|
||||||
"integrity": "sha512-zwxwiQqexizSXFZV13zMiEtW1E3lv7RlUv+1f5FBiR4x7wFhEjm3aFTyYkZQWzyN08WnPdox015GoRH5D/E5YA==",
|
"integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"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