Bundles in-flight ECHODO work with the Coolify deployment configuration: App - New routes: ai, forms, planner, settings (templates/types), teams, doc detail, whiteboard detail - New components: app shell rework (icon-rail, top-header), forms builder/renderer/responses, types manager, objects creation dialog, card primitive, form + overview views - New tRPC routers: favorites, forms, types, workspaces; updates to health and objects routers - Markdown backlog sync (packages/database) + cursor-sync schema/migrations - Schema additions: forms, types, favorites, markdown_backlog, cursor_sync - Initial Drizzle migrations checked in Deployment - docker/docker-compose.coolify.yml: drops bundled Postgres/Redis (uses CT 102 shared services), removes host port mappings, adds Coolify SERVICE_FQDN_* magic vars for web + collab - .env.example rewritten as the full ECHODO/Coolify variable manifest - NextAuth gains an Authentik OIDC provider (gated on env presence) - Root layout injects Umami tracking script when configured; metadata title flipped to ECHODO Security - .gitignore expanded to exclude AGENT-DEPLOY.md, .env.*, secrets/, credentials.*, *.key, *.crt, *.pem, ssh keys Made-with: Cursor
162 lines
5.8 KiB
TypeScript
162 lines
5.8 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useRef, useEffect } from "react";
|
|
import { useParams } from "next/navigation";
|
|
import { Loader2, Send, Sparkles, Bot, User } from "lucide-react";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
type Message = {
|
|
id: string;
|
|
role: "user" | "assistant";
|
|
content: string;
|
|
};
|
|
|
|
export default function AIPage() {
|
|
const params = useParams();
|
|
const workspaceSlug = params.workspaceSlug as string;
|
|
|
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
const [input, setInput] = useState("");
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
|
|
// Auto-scroll on new messages
|
|
useEffect(() => {
|
|
if (scrollRef.current) {
|
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
}
|
|
}, [messages]);
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
const trimmed = input.trim();
|
|
if (!trimmed || isLoading) return;
|
|
|
|
const userMsg: Message = { id: crypto.randomUUID(), role: "user", content: trimmed };
|
|
setMessages((prev) => [...prev, userMsg]);
|
|
setInput("");
|
|
setIsLoading(true);
|
|
|
|
// Placeholder AI response
|
|
setTimeout(() => {
|
|
const assistantMsg: Message = {
|
|
id: crypto.randomUUID(),
|
|
role: "assistant",
|
|
content:
|
|
"I'm your AI assistant. Full AI integration is coming soon! I'll be able to help you create tasks, documents, and manage your workspace.",
|
|
};
|
|
setMessages((prev) => [...prev, assistantMsg]);
|
|
setIsLoading(false);
|
|
}, 1000);
|
|
};
|
|
|
|
return (
|
|
<div className="flex h-full min-h-0 flex-col">
|
|
{/* Header */}
|
|
<div className="flex shrink-0 items-center gap-3 border-b px-6 py-4">
|
|
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10">
|
|
<Sparkles className="size-5 text-primary" />
|
|
</div>
|
|
<div>
|
|
<h1 className="text-lg font-semibold">AI Assistant</h1>
|
|
<p className="text-xs text-muted-foreground">Ask me anything about your workspace</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Messages */}
|
|
<div ref={scrollRef} className="min-h-0 flex-1 overflow-y-auto">
|
|
{messages.length === 0 ? (
|
|
<div className="flex h-full min-h-[12rem] flex-col items-center justify-center gap-4 text-muted-foreground">
|
|
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-primary/10">
|
|
<Bot className="size-8 text-primary" />
|
|
</div>
|
|
<div className="text-center">
|
|
<p className="text-base font-medium">How can I help you today?</p>
|
|
<p className="mt-1 text-sm">
|
|
Ask me to create tasks, summarize documents, or manage your workspace.
|
|
</p>
|
|
</div>
|
|
<div className="mt-2 flex flex-wrap justify-center gap-2">
|
|
{["Create a new project", "Summarize my tasks", "Help me plan a sprint"].map(
|
|
(suggestion) => (
|
|
<button
|
|
key={suggestion}
|
|
type="button"
|
|
onClick={() => setInput(suggestion)}
|
|
className="rounded-full border px-3 py-1.5 text-xs transition-colors hover:bg-muted"
|
|
>
|
|
{suggestion}
|
|
</button>
|
|
),
|
|
)}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="mx-auto max-w-3xl space-y-4 px-4 py-6">
|
|
{messages.map((msg) => (
|
|
<div
|
|
key={msg.id}
|
|
className={cn("flex gap-3", msg.role === "user" && "flex-row-reverse")}
|
|
>
|
|
<div
|
|
className={cn(
|
|
"flex h-8 w-8 shrink-0 items-center justify-center rounded-full",
|
|
msg.role === "user" ? "bg-primary text-primary-foreground" : "bg-muted",
|
|
)}
|
|
>
|
|
{msg.role === "user" ? <User className="size-4" /> : <Bot className="size-4" />}
|
|
</div>
|
|
<div
|
|
className={cn(
|
|
"max-w-[80%] rounded-xl px-4 py-2.5 text-sm",
|
|
msg.role === "user"
|
|
? "bg-primary text-primary-foreground"
|
|
: "bg-muted",
|
|
)}
|
|
>
|
|
{msg.content}
|
|
</div>
|
|
</div>
|
|
))}
|
|
{isLoading && (
|
|
<div className="flex gap-3">
|
|
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted">
|
|
<Bot className="size-4" />
|
|
</div>
|
|
<div className="rounded-xl bg-muted px-4 py-2.5">
|
|
<Loader2 className="size-4 animate-spin" />
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Input */}
|
|
<div className="shrink-0 border-t bg-background p-4">
|
|
<form onSubmit={handleSubmit} className="mx-auto flex max-w-3xl gap-2">
|
|
<textarea
|
|
ref={inputRef}
|
|
value={input}
|
|
onChange={(e) => setInput(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
e.preventDefault();
|
|
void handleSubmit(e);
|
|
}
|
|
}}
|
|
placeholder="Ask anything..."
|
|
className="flex-1 resize-none rounded-lg border bg-background px-4 py-2.5 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
rows={1}
|
|
/>
|
|
<Button type="submit" size="icon" disabled={!input.trim() || isLoading}>
|
|
<Send className="size-4" />
|
|
</Button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|