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,327 @@
'use client';
import { useState } from 'react';
import { ArrowLeft, Copy, Check, Save, Trash2, History, Sparkles } from 'lucide-react';
import { Button } from '@/shared/components/ui/button';
import { Input } from '@/shared/components/ui/input';
import { Label } from '@/shared/components/ui/label';
import { Textarea } from '@/shared/components/ui/textarea';
import { Badge } from '@/shared/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/shared/components/ui/card';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
import { Separator } from '@/shared/components/ui/separator';
import type { PromptTemplate, PromptVariable } from '../types';
import { usePromptGenerator } from '../hooks/use-prompt-generator';
import { cn } from '@/shared/lib/utils';
const CATEGORY_LABELS: Record<string, string> = {
architecture: 'Arhitectură',
legal: 'Legal',
technical: 'Tehnic',
administrative: 'Administrativ',
gis: 'GIS',
bim: 'BIM',
rendering: 'Vizualizare',
procurement: 'Achiziții',
general: 'General',
};
const TARGET_LABELS: Record<string, string> = {
text: 'Text', image: 'Imagine', code: 'Cod', review: 'Review', rewrite: 'Rescriere',
};
type ViewMode = 'templates' | 'compose' | 'history';
export function PromptGeneratorModule() {
const {
allTemplates, selectedTemplate, values, composedPrompt,
history, selectTemplate, updateValue, saveToHistory,
deleteHistoryEntry, clearSelection,
} = usePromptGenerator();
const [viewMode, setViewMode] = useState<ViewMode>('templates');
const [copied, setCopied] = useState(false);
const [saved, setSaved] = useState(false);
const [filterCategory, setFilterCategory] = useState<string>('all');
const handleSelectTemplate = (template: PromptTemplate) => {
selectTemplate(template);
setViewMode('compose');
};
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(composedPrompt);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch { /* silent */ }
};
const handleSave = async () => {
await saveToHistory();
setSaved(true);
setTimeout(() => setSaved(false), 2000);
};
const handleBack = () => {
clearSelection();
setViewMode('templates');
};
const filteredTemplates = filterCategory === 'all'
? allTemplates
: allTemplates.filter((t) => t.category === filterCategory);
const usedCategories = [...new Set(allTemplates.map((t) => t.category))];
return (
<div className="space-y-6">
{/* Navigation */}
<div className="flex items-center gap-2">
<Button
variant={viewMode === 'templates' ? 'default' : 'outline'}
size="sm"
onClick={() => { clearSelection(); setViewMode('templates'); }}
>
<Sparkles className="mr-1 h-3.5 w-3.5" /> Șabloane
</Button>
<Button
variant={viewMode === 'history' ? 'default' : 'outline'}
size="sm"
onClick={() => setViewMode('history')}
>
<History className="mr-1 h-3.5 w-3.5" /> Istoric ({history.length})
</Button>
</div>
{/* Template browser */}
{viewMode === 'templates' && (
<div className="space-y-4">
<div className="flex items-center gap-3">
<Label>Categorie:</Label>
<Select value={filterCategory} onValueChange={setFilterCategory}>
<SelectTrigger className="w-[180px]"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="all">Toate</SelectItem>
{usedCategories.map((cat) => (
<SelectItem key={cat} value={cat}>{CATEGORY_LABELS[cat] ?? cat}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid gap-3 sm:grid-cols-2">
{filteredTemplates.map((template) => (
<Card
key={template.id}
className="cursor-pointer transition-colors hover:border-primary/50 hover:bg-accent/30"
onClick={() => handleSelectTemplate(template)}
>
<CardHeader className="pb-2">
<div className="flex items-center gap-2">
<CardTitle className="text-sm">{template.name}</CardTitle>
<Badge variant="outline" className="text-[10px]">{TARGET_LABELS[template.targetAiType]}</Badge>
</div>
<CardDescription className="text-xs">{template.description}</CardDescription>
</CardHeader>
<CardContent className="pt-0">
<div className="flex flex-wrap gap-1">
<Badge variant="secondary" className="text-[10px]">{CATEGORY_LABELS[template.category] ?? template.category}</Badge>
<Badge variant="secondary" className="text-[10px]">{template.variables.length} variabile</Badge>
<Badge variant="secondary" className="text-[10px]">{template.blocks.length} blocuri</Badge>
</div>
</CardContent>
</Card>
))}
</div>
</div>
)}
{/* Composition view */}
{viewMode === 'compose' && selectedTemplate && (
<div className="space-y-6">
<div className="flex items-center gap-3">
<Button variant="ghost" size="sm" onClick={handleBack}>
<ArrowLeft className="mr-1 h-3.5 w-3.5" /> Înapoi
</Button>
<div>
<h3 className="font-semibold">{selectedTemplate.name}</h3>
<p className="text-xs text-muted-foreground">{selectedTemplate.description}</p>
</div>
</div>
<div className="grid gap-6 lg:grid-cols-2">
{/* Variable form */}
<Card>
<CardHeader><CardTitle className="text-base">Variabile</CardTitle></CardHeader>
<CardContent className="space-y-4">
{selectedTemplate.variables.map((variable) => (
<VariableField
key={variable.id}
variable={variable}
value={values[variable.id]}
onChange={(val) => updateValue(variable.id, val)}
/>
))}
</CardContent>
</Card>
{/* Output */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold">Prompt compus</h3>
<div className="flex gap-1.5">
<Button variant="outline" size="sm" onClick={handleSave} disabled={!composedPrompt}>
<Save className="mr-1 h-3.5 w-3.5" />
{saved ? 'Salvat!' : 'Salvează'}
</Button>
<Button size="sm" onClick={handleCopy} disabled={!composedPrompt}>
{copied ? <Check className="mr-1 h-3.5 w-3.5" /> : <Copy className="mr-1 h-3.5 w-3.5" />}
{copied ? 'Copiat!' : 'Copiază'}
</Button>
</div>
</div>
<div className="min-h-[300px] whitespace-pre-wrap rounded-lg border bg-muted/30 p-4 text-sm">
{composedPrompt || <span className="text-muted-foreground italic">Completează variabilele pentru a genera promptul...</span>}
</div>
<div className="flex flex-wrap gap-1.5">
<Badge variant="outline" className="text-[10px]">Output: {selectedTemplate.outputMode}</Badge>
<Badge variant="outline" className="text-[10px]">Blocuri: {selectedTemplate.blocks.length}</Badge>
<Badge variant="outline" className="text-[10px]">Caractere: {composedPrompt.length}</Badge>
</div>
</div>
</div>
</div>
)}
{/* History view */}
{viewMode === 'history' && (
<div className="space-y-3">
{history.length === 0 ? (
<p className="py-8 text-center text-sm text-muted-foreground">Niciun prompt salvat în istoric.</p>
) : (
history.map((entry) => (
<Card key={entry.id} className="group">
<CardContent className="p-4">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<p className="text-sm font-medium">{entry.templateName}</p>
<Badge variant="outline" className="text-[10px]">{entry.outputMode}</Badge>
</div>
<p className="mt-1 text-xs text-muted-foreground">
{new Date(entry.createdAt).toLocaleString('ro-RO')} {entry.composedPrompt.length} caractere
</p>
<pre className="mt-2 max-h-24 overflow-hidden truncate text-xs text-muted-foreground">
{entry.composedPrompt.slice(0, 200)}...
</pre>
</div>
<div className="flex gap-1 opacity-0 transition-opacity group-hover:opacity-100">
<CopyHistoryButton text={entry.composedPrompt} />
<Button variant="ghost" size="icon" className="h-7 w-7 text-destructive" onClick={() => deleteHistoryEntry(entry.id)}>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
</div>
</CardContent>
</Card>
))
)}
</div>
)}
</div>
);
}
function VariableField({ variable, value, onChange }: {
variable: PromptVariable;
value: unknown;
onChange: (val: unknown) => void;
}) {
const strVal = value !== undefined && value !== null ? String(value) : '';
return (
<div>
<Label className={cn(variable.required && 'after:content-["*"] after:ml-0.5 after:text-destructive')}>
{variable.label}
</Label>
{variable.helperText && (
<p className="text-[11px] text-muted-foreground">{variable.helperText}</p>
)}
{(variable.type === 'text' || variable.type === 'number') && (
<Input
type={variable.type === 'number' ? 'number' : 'text'}
value={strVal}
onChange={(e) => onChange(variable.type === 'number' ? Number(e.target.value) : e.target.value)}
placeholder={variable.placeholder}
className="mt-1"
/>
)}
{(variable.type === 'select' || variable.type === 'tone-selector' || variable.type === 'company-selector') && variable.options && (
<Select value={strVal} onValueChange={onChange}>
<SelectTrigger className="mt-1"><SelectValue placeholder="Selectează..." /></SelectTrigger>
<SelectContent>
{variable.options.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
))}
</SelectContent>
</Select>
)}
{variable.type === 'boolean' && (
<div className="mt-1 flex items-center gap-2">
<input
type="checkbox"
checked={!!value}
onChange={(e) => onChange(e.target.checked)}
className="h-4 w-4 rounded accent-primary"
/>
<span className="text-sm text-muted-foreground">{variable.placeholder ?? 'Da/Nu'}</span>
</div>
)}
{variable.type === 'multi-select' && variable.options && (
<div className="mt-1 flex flex-wrap gap-1.5">
{variable.options.map((opt) => {
const selected = Array.isArray(value) && (value as string[]).includes(opt.value);
return (
<button
key={opt.value}
type="button"
onClick={() => {
const arr = Array.isArray(value) ? [...(value as string[])] : [];
if (selected) onChange(arr.filter((v) => v !== opt.value));
else onChange([...arr, opt.value]);
}}
className={cn(
'rounded-full border px-2.5 py-0.5 text-xs transition-colors',
selected ? 'border-primary bg-primary text-primary-foreground' : 'hover:bg-accent'
)}
>
{opt.label}
</button>
);
})}
</div>
)}
</div>
);
}
function CopyHistoryButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
} catch { /* silent */ }
};
return (
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={handleCopy}>
{copied ? <Check className="h-3.5 w-3.5 text-green-500" /> : <Copy className="h-3.5 w-3.5" />}
</Button>
);
}
+17
View File
@@ -0,0 +1,17 @@
import type { ModuleConfig } from '@/core/module-registry/types';
export const promptGeneratorConfig: ModuleConfig = {
id: 'prompt-generator',
name: 'Generator Prompturi',
description: 'Generator structurat de prompturi pe bază de șabloane parametrizate, organizate pe domenii profesionale',
icon: 'sparkles',
route: '/prompt-generator',
category: 'ai',
featureFlag: 'module.prompt-generator',
visibility: 'all',
version: '0.1.0',
dependencies: [],
storageNamespace: 'prompt-generator',
navOrder: 50,
tags: ['prompt', 'ai', 'generator', 'șabloane'],
};
@@ -0,0 +1,110 @@
'use client';
import { useState, useCallback, useMemo, useEffect } from 'react';
import { useStorage } from '@/core/storage';
import { v4 as uuid } from 'uuid';
import type { PromptTemplate, PromptHistoryEntry, OutputMode } from '../types';
import { BUILTIN_TEMPLATES } from '../services/builtin-templates';
import { composePrompt } from '../services/prompt-composer';
const HISTORY_PREFIX = 'history:';
const TEMPLATE_PREFIX = 'template:';
export function usePromptGenerator() {
const storage = useStorage('prompt-generator');
const [selectedTemplate, setSelectedTemplate] = useState<PromptTemplate | null>(null);
const [values, setValues] = useState<Record<string, unknown>>({});
const [customTemplates, setCustomTemplates] = useState<PromptTemplate[]>([]);
const [history, setHistory] = useState<PromptHistoryEntry[]>([]);
const [loading, setLoading] = useState(true);
const allTemplates = useMemo(() => [...BUILTIN_TEMPLATES, ...customTemplates], [customTemplates]);
// Load custom templates and history
useEffect(() => {
async function load() {
setLoading(true);
const keys = await storage.list();
const templates: PromptTemplate[] = [];
const entries: PromptHistoryEntry[] = [];
for (const key of keys) {
if (key.startsWith(TEMPLATE_PREFIX)) {
const t = await storage.get<PromptTemplate>(key);
if (t) templates.push(t);
} else if (key.startsWith(HISTORY_PREFIX)) {
const h = await storage.get<PromptHistoryEntry>(key);
if (h) entries.push(h);
}
}
entries.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
setCustomTemplates(templates);
setHistory(entries);
setLoading(false);
}
load();
}, [storage]);
const selectTemplate = useCallback((template: PromptTemplate) => {
setSelectedTemplate(template);
// Initialize values with defaults
const defaults: Record<string, unknown> = {};
for (const v of template.variables) {
if (v.defaultValue !== undefined) defaults[v.id] = v.defaultValue;
}
setValues(defaults);
}, []);
const updateValue = useCallback((variableId: string, value: unknown) => {
setValues((prev) => ({ ...prev, [variableId]: value }));
}, []);
const composedPrompt = useMemo(() => {
if (!selectedTemplate) return '';
return composePrompt(selectedTemplate, values);
}, [selectedTemplate, values]);
const saveToHistory = useCallback(async () => {
if (!selectedTemplate || !composedPrompt) return;
const entry: PromptHistoryEntry = {
id: uuid(),
templateId: selectedTemplate.id,
templateName: selectedTemplate.name,
templateVersion: selectedTemplate.version,
values: { ...values },
composedPrompt,
outputMode: selectedTemplate.outputMode,
providerProfile: selectedTemplate.providerProfile ?? null,
safetyBlocks: selectedTemplate.safetyBlocks?.filter((s) => s.enabled).map((s) => s.id) ?? [],
tags: selectedTemplate.tags,
isFavorite: false,
createdAt: new Date().toISOString(),
};
await storage.set(`${HISTORY_PREFIX}${entry.id}`, entry);
setHistory((prev) => [entry, ...prev]);
return entry;
}, [storage, selectedTemplate, values, composedPrompt]);
const deleteHistoryEntry = useCallback(async (id: string) => {
await storage.delete(`${HISTORY_PREFIX}${id}`);
setHistory((prev) => prev.filter((h) => h.id !== id));
}, [storage]);
const clearSelection = useCallback(() => {
setSelectedTemplate(null);
setValues({});
}, []);
return {
allTemplates,
selectedTemplate,
values,
composedPrompt,
history,
loading,
selectTemplate,
updateValue,
saveToHistory,
deleteHistoryEntry,
clearSelection,
};
}
+16
View File
@@ -0,0 +1,16 @@
export { promptGeneratorConfig } from './config';
export { PromptGeneratorModule } from './components/prompt-generator-module';
export type {
BlockType,
OutputMode,
PromptBlock,
PromptCategory,
PromptDomain,
PromptHistoryEntry,
PromptTemplate,
PromptVariable,
ProviderProfile,
SafetyBlock,
SelectOption,
VariableType,
} from './types';
@@ -0,0 +1,146 @@
import type { PromptTemplate } from '../types';
export const BUILTIN_TEMPLATES: PromptTemplate[] = [
{
id: 'arch-description',
name: 'Descriere Proiect Arhitectură',
category: 'architecture',
domain: 'architecture-visualization',
description: 'Generează o descriere narativă a unui proiect de arhitectură pentru documentație sau prezentare.',
targetAiType: 'text',
blocks: [
{ id: 'b1', type: 'role', label: 'Rol', content: 'Ești un arhitect experimentat, specialist în prezentări de proiecte și memorii tehnice.', order: 1, required: true },
{ id: 'b2', type: 'context', label: 'Context', content: 'Proiectul se numește "{{projectName}}" și este situat în {{location}}. Tipul proiectului: {{projectType}}. Suprafața terenului: {{landArea}} mp. Beneficiar: {{clientName}}.', order: 2, required: true },
{ id: 'b3', type: 'task', label: 'Sarcină', content: 'Scrie o descriere narativă a proiectului pentru {{targetDocument}}. Tonul trebuie să fie {{tone}}.', order: 3, required: true },
{ id: 'b4', type: 'constraints', label: 'Constrângeri', content: 'Lungimea textului: {{textLength}}. Limba: română. Include referințe la specificul local și la contextul urbanistic.', order: 4, required: true },
],
variables: [
{ id: 'projectName', label: 'Nume proiect', type: 'text', required: true, placeholder: 'ex: Locuință unifamilială P+1' },
{ id: 'location', label: 'Locație', type: 'text', required: true, placeholder: 'ex: Cluj-Napoca, str. Unirii nr. 3' },
{ id: 'projectType', label: 'Tip proiect', type: 'select', required: true, options: [
{ value: 'residential', label: 'Rezidențial' }, { value: 'commercial', label: 'Comercial' },
{ value: 'mixed', label: 'Mixt' }, { value: 'industrial', label: 'Industrial' },
{ value: 'public', label: 'Public' },
]},
{ id: 'landArea', label: 'Suprafață teren (mp)', type: 'number', required: false, placeholder: '500' },
{ id: 'clientName', label: 'Beneficiar', type: 'text', required: false, placeholder: 'Nume client' },
{ id: 'targetDocument', label: 'Document țintă', type: 'select', required: true, options: [
{ value: 'memoriu tehnic', label: 'Memoriu tehnic' }, { value: 'prezentare client', label: 'Prezentare client' },
{ value: 'documentație urbanism', label: 'Documentație urbanism' },
]},
{ id: 'tone', label: 'Ton', type: 'select', required: true, options: [
{ value: 'tehnic și formal', label: 'Tehnic/Formal' }, { value: 'narativ și inspirațional', label: 'Narativ' },
{ value: 'concis și factual', label: 'Concis' },
]},
{ id: 'textLength', label: 'Lungime', type: 'select', required: true, options: [
{ value: '200-300 cuvinte', label: 'Scurt (200-300 cuvinte)' }, { value: '500-700 cuvinte', label: 'Mediu (500-700)' },
{ value: '1000+ cuvinte', label: 'Lung (1000+)' },
]},
],
outputMode: 'expanded-expert',
tags: ['arhitectură', 'memoriu', 'descriere'],
version: '1.0.0',
author: 'ArchiTools',
visibility: 'all',
},
{
id: 'legal-review',
name: 'Verificare Conformitate Legislativă',
category: 'legal',
domain: 'legal-review',
description: 'Generează un prompt pentru verificarea conformității unui document cu legislația aplicabilă.',
targetAiType: 'review',
blocks: [
{ id: 'b1', type: 'role', label: 'Rol', content: 'Ești un consultant juridic specializat în legislația construcțiilor și urbanismului din România.', order: 1, required: true },
{ id: 'b2', type: 'context', label: 'Context', content: 'Documentul de verificat: {{documentType}}. Faza de proiectare: {{projectPhase}}.', order: 2, required: true },
{ id: 'b3', type: 'task', label: 'Sarcină', content: 'Verifică conformitatea cu: {{regulations}}. Identifică potențiale neconformități și recomandă acțiuni corective.', order: 3, required: true },
],
variables: [
{ id: 'documentType', label: 'Tip document', type: 'select', required: true, options: [
{ value: 'CU', label: 'Certificat Urbanism' }, { value: 'DTAC', label: 'DTAC' },
{ value: 'PT', label: 'Proiect Tehnic' }, { value: 'PUZ', label: 'PUZ' }, { value: 'PUD', label: 'PUD' },
]},
{ id: 'projectPhase', label: 'Fază proiect', type: 'select', required: true, options: [
{ value: 'studiu fezabilitate', label: 'Studiu fezabilitate' }, { value: 'proiectare', label: 'Proiectare' },
{ value: 'autorizare', label: 'Autorizare' }, { value: 'execuție', label: 'Execuție' },
]},
{ id: 'regulations', label: 'Legislație de referință', type: 'text', required: true, placeholder: 'ex: Legea 50/1991, Normativ P118' },
],
outputMode: 'checklist',
tags: ['legal', 'conformitate', 'verificare'],
version: '1.0.0',
author: 'ArchiTools',
visibility: 'all',
},
{
id: 'tech-spec',
name: 'Specificație Tehnică',
category: 'technical',
domain: 'technical-documentation',
description: 'Generează o specificație tehnică pentru un element de proiect.',
targetAiType: 'text',
blocks: [
{ id: 'b1', type: 'role', label: 'Rol', content: 'Ești un inginer de specialitate cu experiență în redactarea specificațiilor tehnice pentru proiecte de construcții.', order: 1, required: true },
{ id: 'b2', type: 'task', label: 'Sarcină', content: 'Redactează o specificație tehnică pentru: {{element}}. Nivelul de detaliu: {{detailLevel}}.', order: 2, required: true },
{ id: 'b3', type: 'format', label: 'Format', content: 'Structurează specificația cu: descriere generală, materiale, dimensiuni, cerințe de performanță, standarde aplicabile.', order: 3, required: true },
],
variables: [
{ id: 'element', label: 'Element de proiect', type: 'text', required: true, placeholder: 'ex: Structură metalică hală industrială' },
{ id: 'detailLevel', label: 'Nivel detaliu', type: 'select', required: true, options: [
{ value: 'orientativ', label: 'Orientativ' }, { value: 'detaliat', label: 'Detaliat' }, { value: 'execuție', label: 'Nivel execuție' },
]},
],
outputMode: 'step-by-step',
tags: ['tehnic', 'specificație', 'documentație'],
version: '1.0.0',
author: 'ArchiTools',
visibility: 'all',
},
{
id: 'image-prompt',
name: 'Prompt Vizualizare Arhitecturală',
category: 'rendering',
domain: 'architecture-visualization',
description: 'Generează un prompt optimizat pentru AI de generare imagini (Midjourney, DALL-E, Stable Diffusion).',
targetAiType: 'image',
blocks: [
{ id: 'b1', type: 'task', label: 'Descriere', content: '{{buildingType}}, {{style}}, situated in {{setting}}.', order: 1, required: true },
{ id: 'b2', type: 'context', label: 'Atmosferă', content: '{{atmosphere}}. {{timeOfDay}} lighting. {{season}}.', order: 2, required: true },
{ id: 'b3', type: 'format', label: 'Parametri tehnici', content: '{{cameraAngle}}, {{renderStyle}}, high quality, detailed, 8k resolution.', order: 3, required: true },
],
variables: [
{ id: 'buildingType', label: 'Tip clădire', type: 'text', required: true, placeholder: 'ex: Modern minimalist villa with flat roof' },
{ id: 'style', label: 'Stil arhitectural', type: 'select', required: true, options: [
{ value: 'modern minimalist', label: 'Modern minimalist' }, { value: 'contemporary', label: 'Contemporan' },
{ value: 'traditional Romanian', label: 'Tradițional românesc' }, { value: 'brutalist', label: 'Brutalist' },
{ value: 'art deco', label: 'Art Deco' },
]},
{ id: 'setting', label: 'Amplasament', type: 'text', required: true, placeholder: 'ex: hillside with forest background' },
{ id: 'atmosphere', label: 'Atmosferă', type: 'select', required: true, options: [
{ value: 'Warm and inviting', label: 'Cald/Primitor' }, { value: 'Dramatic and bold', label: 'Dramatic' },
{ value: 'Serene and peaceful', label: 'Calm/Liniștit' }, { value: 'Urban and dynamic', label: 'Urban/Dinamic' },
]},
{ id: 'timeOfDay', label: 'Moment al zilei', type: 'select', required: true, options: [
{ value: 'golden hour', label: 'Golden hour' }, { value: 'midday', label: 'Amiază' },
{ value: 'blue hour / dusk', label: 'Blue hour' }, { value: 'night with interior lights', label: 'Noapte' },
]},
{ id: 'season', label: 'Anotimp', type: 'select', required: false, options: [
{ value: 'Spring with green vegetation', label: 'Primăvară' }, { value: 'Summer', label: 'Vară' },
{ value: 'Autumn with warm colors', label: 'Toamnă' }, { value: 'Winter with snow', label: 'Iarnă' },
]},
{ id: 'cameraAngle', label: 'Unghi cameră', type: 'select', required: true, options: [
{ value: 'eye-level perspective', label: 'Nivel ochi' }, { value: 'aerial drone view', label: 'Vedere aeriană' },
{ value: 'low angle dramatic', label: 'Unghi jos' }, { value: 'birds-eye plan view', label: 'Vedere de sus' },
]},
{ id: 'renderStyle', label: 'Stil render', type: 'select', required: true, options: [
{ value: 'photorealistic', label: 'Fotorealistic' }, { value: 'architectural illustration', label: 'Ilustrație' },
{ value: 'watercolor sketch', label: 'Acuarelă' }, { value: 'technical section render', label: 'Secțiune' },
]},
],
outputMode: 'short',
tags: ['render', 'imagine', 'vizualizare', 'midjourney'],
version: '1.0.0',
author: 'ArchiTools',
visibility: 'all',
},
];
@@ -0,0 +1,75 @@
import type { PromptTemplate, PromptBlock, OutputMode } from '../types';
const OUTPUT_MODE_INSTRUCTIONS: Record<OutputMode, string> = {
short: 'Răspunde concis, maximum 2-3 paragrafe.',
'expanded-expert': 'Răspunde detaliat, ca expert în domeniu. Folosește terminologie profesională.',
'step-by-step': 'Răspunde pas cu pas, cu numerotare clară.',
'chain-of-thought': 'Gândește cu voce tare. Arată raționamentul pas cu pas înainte de a da răspunsul final.',
'structured-json': 'Răspunde în format JSON structurat.',
checklist: 'Răspunde sub formă de checklist cu casete de bifat (- [ ] item).',
};
function interpolateVariables(content: string, values: Record<string, unknown>): string {
return content.replace(/\{\{(\w+)\}\}/g, (match, key: string) => {
const val = values[key];
if (val === undefined || val === null || val === '') return match;
if (Array.isArray(val)) return val.join(', ');
return String(val);
});
}
function shouldIncludeBlock(block: PromptBlock, values: Record<string, unknown>): boolean {
if (!block.conditional) return true;
const { variableId, operator, value } = block.conditional;
const actual = values[variableId];
switch (operator) {
case 'truthy': return !!actual;
case 'falsy': return !actual;
case 'equals': return String(actual) === value;
case 'notEquals': return String(actual) !== value;
default: return true;
}
}
export function composePrompt(
template: PromptTemplate,
values: Record<string, unknown>,
): string {
const blocks = [...template.blocks]
.sort((a, b) => a.order - b.order)
.filter((block) => shouldIncludeBlock(block, values));
const parts: string[] = [];
for (const block of blocks) {
const content = interpolateVariables(block.content, values).trim();
if (!content) continue;
parts.push(content);
}
// Add output mode instruction
const modeInstruction = OUTPUT_MODE_INSTRUCTIONS[template.outputMode];
if (modeInstruction) {
parts.push(modeInstruction);
}
// Add safety blocks if enabled
if (template.safetyBlocks) {
const enabledSafety = template.safetyBlocks.filter((s) => s.enabled);
if (enabledSafety.length > 0) {
parts.push(enabledSafety.map((s) => s.content).join('\n'));
}
}
return parts.join('\n\n');
}
export function getUnfilledVariables(
template: PromptTemplate,
values: Record<string, unknown>,
): string[] {
return template.variables
.filter((v) => v.required && (values[v.id] === undefined || values[v.id] === '' || values[v.id] === null))
.map((v) => v.label);
}
+165
View File
@@ -0,0 +1,165 @@
import type { Visibility } from '@/core/module-registry/types';
// ---------------------------------------------------------------------------
// Block system
// ---------------------------------------------------------------------------
export type BlockType =
| 'role'
| 'context'
| 'task'
| 'constraints'
| 'format'
| 'checklist'
| 'validation'
| 'output-schema'
| 'custom';
export interface PromptBlock {
id: string;
type: BlockType;
label: string;
content: string;
order: number;
required: boolean;
conditional?: {
variableId: string;
operator: 'equals' | 'notEquals' | 'truthy' | 'falsy';
value?: string;
};
}
// ---------------------------------------------------------------------------
// Variable system
// ---------------------------------------------------------------------------
export type VariableType =
| 'text'
| 'number'
| 'select'
| 'multi-select'
| 'boolean'
| 'tag-selector'
| 'project-selector'
| 'company-selector'
| 'tone-selector'
| 'regulation-set-selector';
export interface SelectOption {
value: string;
label: string;
}
export interface PromptVariable {
id: string;
label: string;
type: VariableType;
required: boolean;
defaultValue?: unknown;
placeholder?: string;
helperText?: string;
options?: SelectOption[];
validation?: {
min?: number;
max?: number;
pattern?: string;
};
}
// ---------------------------------------------------------------------------
// Output & provider
// ---------------------------------------------------------------------------
export type OutputMode =
| 'short'
| 'expanded-expert'
| 'step-by-step'
| 'chain-of-thought'
| 'structured-json'
| 'checklist';
export type PromptCategory =
| 'architecture'
| 'legal'
| 'technical'
| 'administrative'
| 'gis'
| 'bim'
| 'rendering'
| 'procurement'
| 'general';
export type PromptDomain =
| 'architecture-visualization'
| 'technical-documentation'
| 'legal-review'
| 'urbanism-gis'
| 'bim-coordination'
| 'procurement-bidding'
| 'administrative-writing'
| 'general';
export interface ProviderProfile {
id: string;
name: string;
maxLength?: number;
formattingPreference: 'markdown' | 'plain' | 'structured';
instructionStyle: 'direct' | 'conversational' | 'system-prompt';
verbosityLevel: 'concise' | 'standard' | 'detailed';
}
// ---------------------------------------------------------------------------
// Safety blocks
// ---------------------------------------------------------------------------
export interface SafetyBlock {
id: string;
label: string;
content: string;
enabled: boolean;
category: 'legal' | 'tone' | 'citation' | 'disclaimer';
}
// ---------------------------------------------------------------------------
// Template
// ---------------------------------------------------------------------------
export interface PromptTemplate {
id: string;
name: string;
category: PromptCategory;
domain: PromptDomain;
description: string;
targetAiType: 'text' | 'image' | 'code' | 'review' | 'rewrite';
blocks: PromptBlock[];
variables: PromptVariable[];
outputMode: OutputMode;
providerProfile?: string;
safetyBlocks?: SafetyBlock[];
tags: string[];
version: string;
author: string;
visibility: Visibility;
exampleOutputs?: string[];
metadata?: Record<string, unknown>;
}
// ---------------------------------------------------------------------------
// History
// ---------------------------------------------------------------------------
export interface PromptHistoryEntry {
id: string;
templateId: string;
templateName: string;
templateVersion: string;
values: Record<string, unknown>;
composedPrompt: string;
outputMode: OutputMode;
providerProfile: string | null;
safetyBlocks: string[];
tags: string[];
isFavorite: boolean;
createdAt: string;
metadata?: Record<string, unknown>;
}