feat(db): mesh data model — meshes, members, invites, audit log

- pgSchema "mesh" with 4 tables isolating the peer mesh domain
- Enums: visibility, transport, tier, role
- audit_log is metadata-only (E2E encryption enforced at broker/client)
- Cascade on mesh delete, soft-delete via archivedAt/revokedAt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-04 21:19:32 +01:00
commit d3163a5bff
1384 changed files with 314925 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
"use client";
import { motion } from "motion/react";
import { memo } from "react";
import { useTranslation } from "@turbostarter/i18n";
import { cn } from "@turbostarter/ui";
import { Button } from "@turbostarter/ui-web/button";
import { Icons } from "@turbostarter/ui-web/icons";
import { useComposer } from "~/modules/chat/composer/hooks/use-composer";
const examples = [
{
icon: Icons.FileText,
label: "chat.example.summarize.label",
prompt: "chat.example.summarize.prompt",
},
{
icon: Icons.ChartNoAxesColumn,
label: "chat.example.analyze.label",
prompt: "chat.example.analyze.prompt",
},
{
icon: Icons.Code,
label: "chat.example.code.label",
prompt: "chat.example.code.prompt",
},
{
icon: Icons.Zap,
label: "chat.example.brainstorm.label",
prompt: "chat.example.brainstorm.prompt",
},
{
icon: Icons.PackageOpen,
label: "chat.example.surprise.label",
prompt: "chat.example.surprise.prompt",
},
] as const;
interface ExamplesProps {
readonly id?: string;
readonly className?: string;
}
export const Examples = memo<ExamplesProps>(({ className, id }) => {
const { t } = useTranslation("ai");
const { onSubmit } = useComposer({ id });
return (
<div
className={cn(
"flex w-full flex-row flex-wrap items-center justify-center gap-2 px-3 @sm:gap-2",
className,
)}
>
{examples.map(({ icon: Icon, label, prompt }, index) => (
<motion.div
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
initial={{ opacity: 0, y: 3, filter: "blur(4px)" }}
transition={{ delay: index * 0.1 }}
key={label}
>
<Button
variant="outline"
className="text-muted-foreground gap-2 rounded-full"
onClick={() => onSubmit(t(prompt))}
>
<Icon className="size-4" />
<span>{t(label)}</span>
</Button>
</motion.div>
))}
</div>
);
});
Examples.displayName = "Examples";

View File

@@ -0,0 +1,14 @@
import { useTranslation } from "@turbostarter/i18n";
import { getGreeting } from "@turbostarter/shared/utils";
export const Headline = () => {
const { t } = useTranslation(["common", "ai"]);
const { text, emoji } = getGreeting();
return (
<h1 className="leading-tighter flex w-full flex-col items-center justify-center text-center text-2xl tracking-tight @sm:text-3xl @md:text-4xl">
{t(`greeting.${text}`)} {emoji}
<span className="text-muted-foreground">{t("ai:chat.headline")}</span>
</h1>
);
};

View File

@@ -0,0 +1,32 @@
import { memo } from "react";
import { ChatComposer } from "~/modules/chat/composer";
import { ChatDropzone } from "~/modules/chat/composer/dropzone";
import { useComposer } from "~/modules/chat/composer/hooks/use-composer";
import { Examples } from "~/modules/chat/layout/examples";
import { Headline } from "~/modules/chat/layout/headline";
interface NewChatProps {
id: string;
}
export const NewChat = memo<NewChatProps>(({ id }) => {
const { model } = useComposer({ id });
return (
<ChatDropzone disabled={!model?.attachments}>
<div className="mx-auto flex h-full w-full flex-col items-center justify-between gap-6 md:justify-center md:gap-9 md:p-2">
<div className="flex w-full grow items-end">
<Headline />
</div>
<div className="flex w-full grow flex-col items-center justify-between md:flex-col-reverse md:justify-end md:gap-5">
<Examples className="flex" id={id} />
<div className="relative w-full px-3 pb-3">
<ChatComposer id={id} />
</div>
</div>
</div>
</ChatDropzone>
);
});
NewChat.displayName = "NewChat";

View File

@@ -0,0 +1,34 @@
"use client";
import { memo } from "react";
import { ChatComposer } from "~/modules/chat/composer";
import { ChatDropzone } from "~/modules/chat/composer/dropzone";
import { Chat } from "~/modules/chat/thread";
import { useComposer } from "../composer/hooks/use-composer";
import type { ChatMessage } from "@turbostarter/ai/chat/types";
interface ViewChatProps {
readonly id: string;
readonly initialMessages?: ChatMessage[];
}
export const ViewChat = memo<ViewChatProps>(({ id, initialMessages }) => {
const { model } = useComposer({ id, initialMessages });
return (
<ChatDropzone disabled={!model?.attachments}>
<Chat id={id} initialMessages={initialMessages} />
<div className="absolute inset-x-0 bottom-0 z-50 mx-auto max-w-[50rem]">
<div className="relative z-40 flex w-full flex-col items-center px-3 pb-3">
<ChatComposer id={id} initialMessages={initialMessages} />
</div>
</div>
</ChatDropzone>
);
});
ViewChat.displayName = "ViewChat";