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:
Marius Tarau
2026-02-17 12:50:25 +02:00
commit 4c46e8bcdd
189 changed files with 33780 additions and 0 deletions
@@ -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>
);
}
+17
View File
@@ -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'],
};
+93
View File
@@ -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,
};
}
+3
View File
@@ -0,0 +1,3 @@
export { aiChatConfig } from './config';
export { AiChatModule } from './components/ai-chat-module';
export type { ChatMessage, ChatRole, ChatSession } from './types';
+15
View File
@@ -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;
}