Initial commit: ArchiTools modular dashboard platform
Complete Next.js 16 application with 13 fully implemented modules: Email Signature, Word XML Generator, Registratura, Dashboard, Tag Manager, IT Inventory, Address Book, Password Vault, Mini Utilities, Prompt Generator, Digital Signatures, Word Templates, and AI Chat. Includes core platform systems (module registry, feature flags, storage abstraction, i18n, theming, auth stub, tagging), 16 technical documentation files, Docker deployment config, and legacy HTML tool reference. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { Plus, Send, Trash2, MessageSquare, Settings } from 'lucide-react';
|
||||
import { Button } from '@/shared/components/ui/button';
|
||||
import { Input } from '@/shared/components/ui/input';
|
||||
import { Card, CardContent } from '@/shared/components/ui/card';
|
||||
import { Badge } from '@/shared/components/ui/badge';
|
||||
import { cn } from '@/shared/lib/utils';
|
||||
import { useChat } from '../hooks/use-chat';
|
||||
|
||||
export function AiChatModule() {
|
||||
const {
|
||||
sessions, activeSession, activeSessionId,
|
||||
createSession, addMessage, deleteSession, selectSession,
|
||||
} = useChat();
|
||||
|
||||
const [input, setInput] = useState('');
|
||||
const [showConfig, setShowConfig] = useState(false);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [activeSession?.messages.length]);
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!input.trim()) return;
|
||||
const text = input.trim();
|
||||
setInput('');
|
||||
|
||||
if (!activeSessionId) {
|
||||
await createSession();
|
||||
}
|
||||
|
||||
await addMessage(text, 'user');
|
||||
|
||||
// Simulate AI response (no real API connected)
|
||||
setTimeout(async () => {
|
||||
await addMessage(
|
||||
'Acest modul necesită configurarea unei conexiuni API către un model AI (ex: Claude, GPT). ' +
|
||||
'Momentan funcționează în mod demonstrativ — mesajele sunt salvate local, dar răspunsurile AI nu sunt generate.\n\n' +
|
||||
'Pentru a activa răspunsurile AI, configurați cheia API în setările modulului.',
|
||||
'assistant'
|
||||
);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-12rem)] gap-4">
|
||||
{/* Sidebar - sessions */}
|
||||
<div className="hidden w-64 shrink-0 flex-col gap-2 md:flex">
|
||||
<Button onClick={() => createSession()} size="sm" className="w-full">
|
||||
<Plus className="mr-1.5 h-3.5 w-3.5" /> Conversație nouă
|
||||
</Button>
|
||||
|
||||
<div className="flex-1 space-y-1 overflow-y-auto">
|
||||
{sessions.map((session) => (
|
||||
<div
|
||||
key={session.id}
|
||||
className={cn(
|
||||
'group flex cursor-pointer items-center justify-between rounded-md px-2 py-1.5 text-sm transition-colors',
|
||||
session.id === activeSessionId ? 'bg-accent' : 'hover:bg-accent/50'
|
||||
)}
|
||||
onClick={() => selectSession(session.id)}
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
<MessageSquare className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<span className="truncate">{session.title}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 opacity-0 group-hover:opacity-100"
|
||||
onClick={(e) => { e.stopPropagation(); deleteSession(session.id); }}
|
||||
>
|
||||
<Trash2 className="h-3 w-3 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main chat area */}
|
||||
<div className="flex flex-1 flex-col rounded-lg border">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b px-4 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-sm font-medium">
|
||||
{activeSession?.title ?? 'Chat AI'}
|
||||
</h3>
|
||||
<Badge variant="outline" className="text-[10px]">Demo</Badge>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => setShowConfig(!showConfig)}>
|
||||
<Settings className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Config banner */}
|
||||
{showConfig && (
|
||||
<div className="border-b bg-muted/30 px-4 py-3 text-xs text-muted-foreground">
|
||||
<p className="font-medium">Configurare API (viitor)</p>
|
||||
<p className="mt-1">
|
||||
Modulul va suporta conectarea la API-uri AI (Anthropic Claude, OpenAI, modele locale via Ollama).
|
||||
Cheia API și endpoint-ul se vor configura din setările aplicației sau variabile de mediu.
|
||||
</p>
|
||||
<p className="mt-1">
|
||||
Momentan, conversațiile sunt salvate local, dar fără generare de răspunsuri AI reale.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{!activeSession || activeSession.messages.length === 0 ? (
|
||||
<div className="flex h-full flex-col items-center justify-center text-muted-foreground">
|
||||
<MessageSquare className="mb-3 h-10 w-10 opacity-30" />
|
||||
<p className="text-sm">Începe o conversație nouă</p>
|
||||
<p className="mt-1 text-xs">Scrie un mesaj sau creează o sesiune nouă din bara laterală.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{activeSession.messages.map((msg) => (
|
||||
<div
|
||||
key={msg.id}
|
||||
className={cn(
|
||||
'max-w-[80%] rounded-lg px-3 py-2 text-sm',
|
||||
msg.role === 'user'
|
||||
? 'ml-auto bg-primary text-primary-foreground'
|
||||
: 'bg-muted'
|
||||
)}
|
||||
>
|
||||
<p className="whitespace-pre-wrap">{msg.content}</p>
|
||||
<p className={cn(
|
||||
'mt-1 text-[10px]',
|
||||
msg.role === 'user' ? 'text-primary-foreground/60' : 'text-muted-foreground'
|
||||
)}>
|
||||
{new Date(msg.timestamp).toLocaleTimeString('ro-RO', { hour: '2-digit', minute: '2-digit' })}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div className="border-t p-3">
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && !e.shiftKey && handleSend()}
|
||||
placeholder="Scrie un mesaj..."
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button onClick={handleSend} disabled={!input.trim()}>
|
||||
<Send className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { ModuleConfig } from '@/core/module-registry/types';
|
||||
|
||||
export const aiChatConfig: ModuleConfig = {
|
||||
id: 'ai-chat',
|
||||
name: 'Chat AI',
|
||||
description: 'Interfață de conversație cu modele AI pentru asistență profesională',
|
||||
icon: 'message-square',
|
||||
route: '/ai-chat',
|
||||
category: 'ai',
|
||||
featureFlag: 'module.ai-chat',
|
||||
visibility: 'all',
|
||||
version: '0.1.0',
|
||||
dependencies: [],
|
||||
storageNamespace: 'ai-chat',
|
||||
navOrder: 51,
|
||||
tags: ['chat', 'ai', 'conversație'],
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useStorage } from '@/core/storage';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import type { ChatMessage, ChatSession } from '../types';
|
||||
|
||||
const SESSION_PREFIX = 'session:';
|
||||
|
||||
export function useChat() {
|
||||
const storage = useStorage('ai-chat');
|
||||
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
||||
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const activeSession = sessions.find((s) => s.id === activeSessionId) ?? null;
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true);
|
||||
const keys = await storage.list();
|
||||
const results: ChatSession[] = [];
|
||||
for (const key of keys) {
|
||||
if (key.startsWith(SESSION_PREFIX)) {
|
||||
const session = await storage.get<ChatSession>(key);
|
||||
if (session) results.push(session);
|
||||
}
|
||||
}
|
||||
results.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
||||
setSessions(results);
|
||||
setLoading(false);
|
||||
}, [storage]);
|
||||
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
useEffect(() => { refresh(); }, [refresh]);
|
||||
|
||||
const createSession = useCallback(async (title?: string) => {
|
||||
const session: ChatSession = {
|
||||
id: uuid(),
|
||||
title: title || `Conversație ${new Date().toLocaleDateString('ro-RO')}`,
|
||||
messages: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
await storage.set(`${SESSION_PREFIX}${session.id}`, session);
|
||||
setSessions((prev) => [session, ...prev]);
|
||||
setActiveSessionId(session.id);
|
||||
return session;
|
||||
}, [storage]);
|
||||
|
||||
const addMessage = useCallback(async (content: string, role: ChatMessage['role']) => {
|
||||
if (!activeSessionId) return;
|
||||
const message: ChatMessage = {
|
||||
id: uuid(),
|
||||
role,
|
||||
content,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
setSessions((prev) => prev.map((s) => {
|
||||
if (s.id !== activeSessionId) return s;
|
||||
return { ...s, messages: [...s.messages, message] };
|
||||
}));
|
||||
// Persist
|
||||
const current = sessions.find((s) => s.id === activeSessionId);
|
||||
if (current) {
|
||||
const updated = { ...current, messages: [...current.messages, message] };
|
||||
await storage.set(`${SESSION_PREFIX}${activeSessionId}`, updated);
|
||||
}
|
||||
return message;
|
||||
}, [storage, activeSessionId, sessions]);
|
||||
|
||||
const deleteSession = useCallback(async (id: string) => {
|
||||
await storage.delete(`${SESSION_PREFIX}${id}`);
|
||||
setSessions((prev) => prev.filter((s) => s.id !== id));
|
||||
if (activeSessionId === id) {
|
||||
setActiveSessionId(null);
|
||||
}
|
||||
}, [storage, activeSessionId]);
|
||||
|
||||
const selectSession = useCallback((id: string) => {
|
||||
setActiveSessionId(id);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
sessions,
|
||||
activeSession,
|
||||
activeSessionId,
|
||||
loading,
|
||||
createSession,
|
||||
addMessage,
|
||||
deleteSession,
|
||||
selectSession,
|
||||
refresh,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { aiChatConfig } from './config';
|
||||
export { AiChatModule } from './components/ai-chat-module';
|
||||
export type { ChatMessage, ChatRole, ChatSession } from './types';
|
||||
@@ -0,0 +1,15 @@
|
||||
export type ChatRole = 'user' | 'assistant';
|
||||
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
role: ChatRole;
|
||||
content: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface ChatSession {
|
||||
id: string;
|
||||
title: string;
|
||||
messages: ChatMessage[];
|
||||
createdAt: string;
|
||||
}
|
||||
Reference in New Issue
Block a user