feat(3.15): AI Tools extindere si integrare
Prompt Generator: - Search bar cu cautare in name/description/tags/category - Filtru target type (text/image) cu toggle rapid 'Imagine' - 4 template-uri noi imagine: Midjourney Exterior, SD Interior, Midjourney Infographic, SD Material Texture (18 total) - Config v0.2.0 AI Chat Real API Integration: - /api/ai-chat route: multi-provider (OpenAI, Anthropic, Ollama, demo) - System prompt default in romana pt context arhitectura - GET: config status, POST: message routing - use-chat.ts: sendMessage() cu fetch real, sending state, providerConfig fetch, updateSession() pt project linking - UI: provider status badge (Wifi/WifiOff), Bot icon pe mesaje, loading spinner la generare, disable input while sending - Config banner cu detalii provider/model/stare AI Chat + Tag Manager: - Project selector dropdown in chat header (useTags project) - Session linking: projectTagId + projectName on ChatSession - Project name display in session sidebar - Project context injected in system prompt Docker: - AI env vars: AI_PROVIDER, AI_API_KEY, AI_MODEL, AI_BASE_URL, AI_MAX_TOKENS
This commit is contained in:
+10
-5
@@ -385,13 +385,18 @@
|
|||||||
- ✅ **Extractor Paletă Culori:** Upload imagine → canvas downscale → top 8 culori ca swatches hex clickabile
|
- ✅ **Extractor Paletă Culori:** Upload imagine → canvas downscale → top 8 culori ca swatches hex clickabile
|
||||||
- ⏭️ **DWG to DXF:** Amânat — necesită serviciu backend (ODA File Converter sau similar)
|
- ⏭️ **DWG to DXF:** Amânat — necesită serviciu backend (ODA File Converter sau similar)
|
||||||
|
|
||||||
### 3.13 `[BUSINESS]` Tag Manager — Sincronizare ManicTime
|
### 3.13 ✅ `[BUSINESS]` Tag Manager — Sincronizare ManicTime (11b35c7)
|
||||||
|
|
||||||
**Cerințe noi:**
|
**Implementat:**
|
||||||
|
|
||||||
- **Sincronizare Bidirecțională:** Modulul trebuie să citească și să scrie direct în fișierul text folosit de ManicTime (`\\time\tags\Tags.txt` aflat pe serverul Windows 10 Enterprise LTSC). Orice tag adăugat în ArchiTools trebuie să apară în ManicTime și invers.
|
- ✅ **Sincronizare Bidirecțională:** Parser service (`manictime-service.ts`) parsează/serializează Tags.txt; API route `/api/manictime` cu GET (citire + plan sync) + POST (pull/push/both); UI panel cu buton verificare, import, export, sincronizare completă
|
||||||
- **Versionare Fișier (Backup):** La fiecare modificare a fișierului `Tags.txt` din ArchiTools, sistemul trebuie să creeze automat un backup al versiunii anterioare (ex: `Tags_backup_YYYYMMDD_HHMMSS.txt`) în același folder, pentru a preveni pierderea accidentală a structurii de tag-uri.
|
- ✅ **Versionare Fișier (Backup):** Backup automat `Tags_backup_YYYYMMDD_HHMMSS.txt` la fiecare scriere (push/both)
|
||||||
- **Validare Ierarhie:** Asigurarea că tag-urile adăugate respectă formatul ierarhic cerut de ManicTime (ex: `Proiect, Faza, Activitate`).
|
- ✅ **Validare Ierarhie:** Verificare cod proiect, asociere companie, detectare duplicate; avertizări afișate în UI
|
||||||
|
- ✅ **Docker config:** `MANICTIME_TAGS_PATH` env var, volume mount `/mnt/manictime`; necesită configurare SMB pe host
|
||||||
|
|
||||||
|
**Infrastructură necesară (pe server):**
|
||||||
|
|
||||||
|
- Montare SMB share: `//time/tags → /mnt/manictime` pe host-ul Ubuntu (cifs-utils)
|
||||||
|
|
||||||
### 3.14 `[ARCHITECTURE]` Storage & Securitate ✅
|
### 3.14 `[ARCHITECTURE]` Storage & Securitate ✅
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ services:
|
|||||||
- ENCRYPTION_SECRET=ArchiTools-Vault-2025!SecureKey@AES256
|
- ENCRYPTION_SECRET=ArchiTools-Vault-2025!SecureKey@AES256
|
||||||
# ManicTime Tags.txt sync (SMB mount path)
|
# ManicTime Tags.txt sync (SMB mount path)
|
||||||
- MANICTIME_TAGS_PATH=/mnt/manictime/Tags.txt
|
- MANICTIME_TAGS_PATH=/mnt/manictime/Tags.txt
|
||||||
|
# AI Chat (set AI_PROVIDER to openai/anthropic/ollama; demo if no key)
|
||||||
|
- AI_PROVIDER=${AI_PROVIDER:-demo}
|
||||||
|
- AI_API_KEY=${AI_API_KEY:-}
|
||||||
|
- AI_MODEL=${AI_MODEL:-}
|
||||||
|
- AI_BASE_URL=${AI_BASE_URL:-}
|
||||||
|
- AI_MAX_TOKENS=${AI_MAX_TOKENS:-2048}
|
||||||
volumes:
|
volumes:
|
||||||
# SMB share for ManicTime Tags.txt (mount on host: //time/tags → /mnt/manictime)
|
# SMB share for ManicTime Tags.txt (mount on host: //time/tags → /mnt/manictime)
|
||||||
- /mnt/manictime:/mnt/manictime
|
- /mnt/manictime:/mnt/manictime
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI Chat API Route
|
||||||
|
*
|
||||||
|
* Supports multiple providers: OpenAI, Anthropic (Claude), Ollama (local).
|
||||||
|
* Provider and API key configured via environment variables:
|
||||||
|
*
|
||||||
|
* - AI_PROVIDER: 'openai' | 'anthropic' | 'ollama' (default: 'demo')
|
||||||
|
* - AI_API_KEY: API key for OpenAI or Anthropic
|
||||||
|
* - AI_MODEL: Model name (default: per provider)
|
||||||
|
* - AI_BASE_URL: Custom base URL (required for Ollama, optional for others)
|
||||||
|
* - AI_MAX_TOKENS: Max response tokens (default: 2048)
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface ChatRequestBody {
|
||||||
|
messages: Array<{
|
||||||
|
role: "user" | "assistant" | "system";
|
||||||
|
content: string;
|
||||||
|
}>;
|
||||||
|
systemPrompt?: string;
|
||||||
|
maxTokens?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConfig() {
|
||||||
|
return {
|
||||||
|
provider: (process.env.AI_PROVIDER ?? "demo") as string,
|
||||||
|
apiKey: process.env.AI_API_KEY ?? "",
|
||||||
|
model: process.env.AI_MODEL ?? "",
|
||||||
|
baseUrl: process.env.AI_BASE_URL ?? "",
|
||||||
|
maxTokens: parseInt(process.env.AI_MAX_TOKENS ?? "2048", 10),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_SYSTEM_PROMPT = `Ești un asistent AI pentru un birou de arhitectură. Răspunzi în limba română.
|
||||||
|
Ești specializat în:
|
||||||
|
- Arhitectură și proiectare
|
||||||
|
- Urbanism și PUZ/PUG/PUD
|
||||||
|
- Legislația construcțiilor din România (Legea 50/1991, Legea 350/2001)
|
||||||
|
- Certificat de Urbanism, Autorizație de Construire
|
||||||
|
- Norme tehnice (P118, normative de proiectare)
|
||||||
|
- Documentație tehnică (DTAC, PT, memorii)
|
||||||
|
Răspunde clar, concis și profesional.`;
|
||||||
|
|
||||||
|
async function callOpenAI(
|
||||||
|
messages: ChatRequestBody["messages"],
|
||||||
|
systemPrompt: string,
|
||||||
|
config: ReturnType<typeof getConfig>,
|
||||||
|
): Promise<string> {
|
||||||
|
const baseUrl = config.baseUrl || "https://api.openai.com/v1";
|
||||||
|
const model = config.model || "gpt-4o-mini";
|
||||||
|
|
||||||
|
const response = await fetch(`${baseUrl}/chat/completions`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${config.apiKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model,
|
||||||
|
messages: [{ role: "system", content: systemPrompt }, ...messages],
|
||||||
|
max_tokens: config.maxTokens,
|
||||||
|
temperature: 0.7,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.text();
|
||||||
|
throw new Error(`OpenAI API error (${response.status}): ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as {
|
||||||
|
choices: Array<{ message: { content: string } }>;
|
||||||
|
};
|
||||||
|
return data.choices[0]?.message?.content ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function callAnthropic(
|
||||||
|
messages: ChatRequestBody["messages"],
|
||||||
|
systemPrompt: string,
|
||||||
|
config: ReturnType<typeof getConfig>,
|
||||||
|
): Promise<string> {
|
||||||
|
const model = config.model || "claude-sonnet-4-20250514";
|
||||||
|
|
||||||
|
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-api-key": config.apiKey,
|
||||||
|
"anthropic-version": "2023-06-01",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model,
|
||||||
|
max_tokens: config.maxTokens,
|
||||||
|
system: systemPrompt,
|
||||||
|
messages: messages.map((m) => ({
|
||||||
|
role: m.role === "system" ? "user" : m.role,
|
||||||
|
content: m.content,
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.text();
|
||||||
|
throw new Error(`Anthropic API error (${response.status}): ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as {
|
||||||
|
content: Array<{ type: string; text: string }>;
|
||||||
|
};
|
||||||
|
return data.content
|
||||||
|
.filter((c) => c.type === "text")
|
||||||
|
.map((c) => c.text)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function callOllama(
|
||||||
|
messages: ChatRequestBody["messages"],
|
||||||
|
systemPrompt: string,
|
||||||
|
config: ReturnType<typeof getConfig>,
|
||||||
|
): Promise<string> {
|
||||||
|
const baseUrl = config.baseUrl || "http://localhost:11434";
|
||||||
|
const model = config.model || "llama3.2";
|
||||||
|
|
||||||
|
const response = await fetch(`${baseUrl}/api/chat`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
model,
|
||||||
|
messages: [{ role: "system", content: systemPrompt }, ...messages],
|
||||||
|
stream: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.text();
|
||||||
|
throw new Error(`Ollama API error (${response.status}): ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as {
|
||||||
|
message: { content: string };
|
||||||
|
};
|
||||||
|
return data.message?.content ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDemoResponse(): string {
|
||||||
|
const responses = [
|
||||||
|
"Modulul AI Chat funcționează în mod demonstrativ. Pentru a activa răspunsuri reale, configurați variabilele de mediu:\n\n" +
|
||||||
|
"- `AI_PROVIDER`: openai / anthropic / ollama\n" +
|
||||||
|
"- `AI_API_KEY`: cheia API\n" +
|
||||||
|
"- `AI_MODEL`: modelul dorit (opțional)\n\n" +
|
||||||
|
"Consultați documentația pentru detalii.",
|
||||||
|
"Aceasta este o conversație demonstrativă. Mesajele sunt salvate, dar răspunsurile AI nu sunt generate fără o conexiune API configurată.",
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
responses[Math.floor(Math.random() * responses.length)] ?? responses[0]!
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/ai-chat — Return provider config (without API key)
|
||||||
|
*/
|
||||||
|
export async function GET() {
|
||||||
|
const config = getConfig();
|
||||||
|
return NextResponse.json({
|
||||||
|
provider: config.provider,
|
||||||
|
model: config.model || "(default)",
|
||||||
|
baseUrl: config.baseUrl || "(default)",
|
||||||
|
maxTokens: config.maxTokens,
|
||||||
|
isConfigured:
|
||||||
|
config.provider !== "demo" &&
|
||||||
|
(config.provider === "ollama" || !!config.apiKey),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/ai-chat — Send messages and get AI response
|
||||||
|
*/
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
const config = getConfig();
|
||||||
|
|
||||||
|
let body: ChatRequestBody;
|
||||||
|
try {
|
||||||
|
body = (await request.json()) as ChatRequestBody;
|
||||||
|
} catch {
|
||||||
|
return NextResponse.json({ error: "invalid_json" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!body.messages || body.messages.length === 0) {
|
||||||
|
return NextResponse.json({ error: "no_messages" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const systemPrompt = body.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let responseText: string;
|
||||||
|
|
||||||
|
switch (config.provider) {
|
||||||
|
case "openai":
|
||||||
|
if (!config.apiKey) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: "missing_api_key",
|
||||||
|
message: "AI_API_KEY nu este configurat.",
|
||||||
|
},
|
||||||
|
{ status: 500 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
responseText = await callOpenAI(body.messages, systemPrompt, config);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "anthropic":
|
||||||
|
if (!config.apiKey) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: "missing_api_key",
|
||||||
|
message: "AI_API_KEY nu este configurat.",
|
||||||
|
},
|
||||||
|
{ status: 500 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
responseText = await callAnthropic(body.messages, systemPrompt, config);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "ollama":
|
||||||
|
responseText = await callOllama(body.messages, systemPrompt, config);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Demo mode
|
||||||
|
responseText = getDemoResponse();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
content: responseText,
|
||||||
|
provider: config.provider,
|
||||||
|
model: config.model || "(default)",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: "api_error",
|
||||||
|
message:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Eroare necunoscută la apelul API AI.",
|
||||||
|
provider: config.provider,
|
||||||
|
},
|
||||||
|
{ status: 502 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
manicTimeTagToCreateData,
|
manicTimeTagToCreateData,
|
||||||
} from "@/modules/tag-manager/services/manictime-service";
|
} from "@/modules/tag-manager/services/manictime-service";
|
||||||
import type { Tag } from "@/core/tagging/types";
|
import type { Tag } from "@/core/tagging/types";
|
||||||
import { prisma } from "@/core/storage/prisma";import type { Prisma } from '@prisma/client';
|
import { prisma } from "@/core/storage/prisma";
|
||||||
|
import type { Prisma } from "@prisma/client";
|
||||||
const NAMESPACE = "tags";
|
const NAMESPACE = "tags";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -155,7 +156,9 @@ export async function POST(request: NextRequest) {
|
|||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const tagJson = JSON.parse(JSON.stringify(tag)) as Prisma.InputJsonValue;
|
const tagJson = JSON.parse(
|
||||||
|
JSON.stringify(tag),
|
||||||
|
) as Prisma.InputJsonValue;
|
||||||
|
|
||||||
await prisma.keyValueStore.upsert({
|
await prisma.keyValueStore.upsert({
|
||||||
where: { namespace_key: { namespace: NAMESPACE, key: id } },
|
where: { namespace_key: { namespace: NAMESPACE, key: id } },
|
||||||
|
|||||||
@@ -1,48 +1,73 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect } from "react";
|
||||||
import { Plus, Send, Trash2, MessageSquare, Settings } from 'lucide-react';
|
import {
|
||||||
import { Button } from '@/shared/components/ui/button';
|
Plus,
|
||||||
import { Input } from '@/shared/components/ui/input';
|
Send,
|
||||||
import { Card, CardContent } from '@/shared/components/ui/card';
|
Trash2,
|
||||||
import { Badge } from '@/shared/components/ui/badge';
|
MessageSquare,
|
||||||
import { cn } from '@/shared/lib/utils';
|
Settings,
|
||||||
import { useChat } from '../hooks/use-chat';
|
Loader2,
|
||||||
|
Bot,
|
||||||
|
Wifi,
|
||||||
|
WifiOff,
|
||||||
|
FolderOpen,
|
||||||
|
X,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { Input } from "@/shared/components/ui/input";
|
||||||
|
import { Badge } from "@/shared/components/ui/badge";
|
||||||
|
import { cn } from "@/shared/lib/utils";
|
||||||
|
import { useChat } from "../hooks/use-chat";
|
||||||
|
import { useTags } from "@/core/tagging/use-tags";
|
||||||
|
|
||||||
|
const PROVIDER_LABELS: Record<string, string> = {
|
||||||
|
openai: "OpenAI",
|
||||||
|
anthropic: "Claude",
|
||||||
|
ollama: "Ollama",
|
||||||
|
demo: "Demo",
|
||||||
|
};
|
||||||
|
|
||||||
export function AiChatModule() {
|
export function AiChatModule() {
|
||||||
const {
|
const {
|
||||||
sessions, activeSession, activeSessionId,
|
sessions,
|
||||||
createSession, addMessage, deleteSession, selectSession,
|
activeSession,
|
||||||
|
activeSessionId,
|
||||||
|
sending,
|
||||||
|
providerConfig,
|
||||||
|
createSession,
|
||||||
|
updateSession,
|
||||||
|
sendMessage,
|
||||||
|
deleteSession,
|
||||||
|
selectSession,
|
||||||
} = useChat();
|
} = useChat();
|
||||||
|
|
||||||
const [input, setInput] = useState('');
|
const { tags: projectTags } = useTags("project");
|
||||||
|
|
||||||
|
const [input, setInput] = useState("");
|
||||||
const [showConfig, setShowConfig] = useState(false);
|
const [showConfig, setShowConfig] = useState(false);
|
||||||
|
const [showProjectPicker, setShowProjectPicker] = useState(false);
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
}, [activeSession?.messages.length]);
|
}, [activeSession?.messages.length]);
|
||||||
|
|
||||||
|
const providerLabel = providerConfig
|
||||||
|
? (PROVIDER_LABELS[providerConfig.provider] ?? providerConfig.provider)
|
||||||
|
: "Demo";
|
||||||
|
const isConfigured = providerConfig?.isConfigured ?? false;
|
||||||
|
|
||||||
const handleSend = async () => {
|
const handleSend = async () => {
|
||||||
if (!input.trim()) return;
|
if (!input.trim() || sending) return;
|
||||||
const text = input.trim();
|
const text = input.trim();
|
||||||
setInput('');
|
setInput("");
|
||||||
|
|
||||||
if (!activeSessionId) {
|
if (!activeSessionId) {
|
||||||
await createSession();
|
await createSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
await addMessage(text, 'user');
|
await sendMessage(text);
|
||||||
|
|
||||||
// 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 (
|
return (
|
||||||
@@ -58,20 +83,32 @@ export function AiChatModule() {
|
|||||||
<div
|
<div
|
||||||
key={session.id}
|
key={session.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
'group flex cursor-pointer items-center justify-between rounded-md px-2 py-1.5 text-sm transition-colors',
|
"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'
|
session.id === activeSessionId
|
||||||
|
? "bg-accent"
|
||||||
|
: "hover:bg-accent/50",
|
||||||
)}
|
)}
|
||||||
onClick={() => selectSession(session.id)}
|
onClick={() => selectSession(session.id)}
|
||||||
>
|
>
|
||||||
<div className="flex min-w-0 items-center gap-1.5">
|
<div className="flex min-w-0 flex-col">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
<MessageSquare className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
<MessageSquare className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||||
<span className="truncate">{session.title}</span>
|
<span className="truncate">{session.title}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{session.projectName && (
|
||||||
|
<span className="ml-5 truncate text-[10px] text-muted-foreground">
|
||||||
|
{session.projectName}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-6 w-6 opacity-0 group-hover:opacity-100"
|
className="h-6 w-6 opacity-0 group-hover:opacity-100"
|
||||||
onClick={(e) => { e.stopPropagation(); deleteSession(session.id); }}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
deleteSession(session.id);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3 w-3 text-destructive" />
|
<Trash2 className="h-3 w-3 text-destructive" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -86,11 +123,103 @@ export function AiChatModule() {
|
|||||||
<div className="flex items-center justify-between border-b px-4 py-2">
|
<div className="flex items-center justify-between border-b px-4 py-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h3 className="text-sm font-medium">
|
<h3 className="text-sm font-medium">
|
||||||
{activeSession?.title ?? 'Chat AI'}
|
{activeSession?.title ?? "Chat AI"}
|
||||||
</h3>
|
</h3>
|
||||||
<Badge variant="outline" className="text-[10px]">Demo</Badge>
|
{/* Project link */}
|
||||||
|
{activeSession && (
|
||||||
|
<div className="relative">
|
||||||
|
{activeSession.projectName ? (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="cursor-pointer gap-1 text-[10px]"
|
||||||
|
onClick={() => setShowProjectPicker(!showProjectPicker)}
|
||||||
|
>
|
||||||
|
<FolderOpen className="h-2.5 w-2.5" />
|
||||||
|
{activeSession.projectName}
|
||||||
|
<X
|
||||||
|
className="h-2.5 w-2.5 opacity-60 hover:opacity-100"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
updateSession(activeSession.id, {
|
||||||
|
projectTagId: undefined,
|
||||||
|
projectName: undefined,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 gap-1 px-2 text-[10px] text-muted-foreground"
|
||||||
|
onClick={() => setShowProjectPicker(!showProjectPicker)}
|
||||||
|
>
|
||||||
|
<FolderOpen className="h-3 w-3" />
|
||||||
|
Proiect
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{showProjectPicker && projectTags.length > 0 && (
|
||||||
|
<div className="absolute left-0 top-full z-50 mt-1 max-h-48 w-52 overflow-y-auto rounded-md border bg-popover p-1 shadow-md">
|
||||||
|
{projectTags.map((tag) => (
|
||||||
|
<button
|
||||||
|
key={tag.id}
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
"flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left text-xs hover:bg-accent",
|
||||||
|
activeSession.projectTagId === tag.id &&
|
||||||
|
"bg-accent font-medium",
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
updateSession(activeSession.id, {
|
||||||
|
projectTagId: tag.id,
|
||||||
|
projectName: tag.projectCode
|
||||||
|
? `${tag.projectCode} — ${tag.label}`
|
||||||
|
: tag.label,
|
||||||
|
});
|
||||||
|
setShowProjectPicker(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tag.color && (
|
||||||
|
<span
|
||||||
|
className="h-2 w-2 rounded-full"
|
||||||
|
style={{ backgroundColor: tag.color }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span className="truncate">
|
||||||
|
{tag.projectCode && (
|
||||||
|
<span className="mr-1 font-mono text-muted-foreground">
|
||||||
|
{tag.projectCode}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{tag.label}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => setShowConfig(!showConfig)}>
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Badge
|
||||||
|
variant={isConfigured ? "default" : "outline"}
|
||||||
|
className={cn(
|
||||||
|
"text-[10px]",
|
||||||
|
isConfigured && "bg-green-600 hover:bg-green-700",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isConfigured ? (
|
||||||
|
<Wifi className="mr-1 h-2.5 w-2.5" />
|
||||||
|
) : (
|
||||||
|
<WifiOff className="mr-1 h-2.5 w-2.5" />
|
||||||
|
)}
|
||||||
|
{providerLabel}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-7 w-7"
|
||||||
|
onClick={() => setShowConfig(!showConfig)}
|
||||||
|
>
|
||||||
<Settings className="h-3.5 w-3.5" />
|
<Settings className="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -98,14 +227,42 @@ export function AiChatModule() {
|
|||||||
{/* Config banner */}
|
{/* Config banner */}
|
||||||
{showConfig && (
|
{showConfig && (
|
||||||
<div className="border-b bg-muted/30 px-4 py-3 text-xs text-muted-foreground">
|
<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="font-medium">Configurare API</p>
|
||||||
<p className="mt-1">
|
{providerConfig ? (
|
||||||
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.
|
<div className="mt-2 grid grid-cols-2 gap-x-4 gap-y-1">
|
||||||
</p>
|
<span>Provider:</span>
|
||||||
<p className="mt-1">
|
<span className="font-medium text-foreground">
|
||||||
Momentan, conversațiile sunt salvate local, dar fără generare de răspunsuri AI reale.
|
{providerLabel}
|
||||||
|
</span>
|
||||||
|
<span>Model:</span>
|
||||||
|
<span className="font-medium text-foreground">
|
||||||
|
{providerConfig.model}
|
||||||
|
</span>
|
||||||
|
<span>Max tokens:</span>
|
||||||
|
<span className="font-medium text-foreground">
|
||||||
|
{providerConfig.maxTokens}
|
||||||
|
</span>
|
||||||
|
<span>Stare:</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"font-medium",
|
||||||
|
isConfigured ? "text-green-600" : "text-amber-600",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isConfigured ? "Configurat" : "Neconfigurat (mod demo)"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{!isConfigured && (
|
||||||
|
<p className="mt-2 text-amber-600">
|
||||||
|
Setați variabilele de mediu AI_PROVIDER, AI_API_KEY și
|
||||||
|
AI_MODEL pentru a activa răspunsurile AI reale.
|
||||||
</p>
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="mt-1">Se verifică conexiunea la API...</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -115,7 +272,9 @@ export function AiChatModule() {
|
|||||||
<div className="flex h-full flex-col items-center justify-center text-muted-foreground">
|
<div className="flex h-full flex-col items-center justify-center text-muted-foreground">
|
||||||
<MessageSquare className="mb-3 h-10 w-10 opacity-30" />
|
<MessageSquare className="mb-3 h-10 w-10 opacity-30" />
|
||||||
<p className="text-sm">Începe o conversație nouă</p>
|
<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>
|
<p className="mt-1 text-xs">
|
||||||
|
Scrie un mesaj sau creează o sesiune nouă din bara laterală.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -123,21 +282,37 @@ export function AiChatModule() {
|
|||||||
<div
|
<div
|
||||||
key={msg.id}
|
key={msg.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
'max-w-[80%] rounded-lg px-3 py-2 text-sm',
|
"max-w-[80%] rounded-lg px-3 py-2 text-sm",
|
||||||
msg.role === 'user'
|
msg.role === "user"
|
||||||
? 'ml-auto bg-primary text-primary-foreground'
|
? "ml-auto bg-primary text-primary-foreground"
|
||||||
: 'bg-muted'
|
: "bg-muted",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{msg.role === "assistant" && (
|
||||||
|
<Bot className="mb-1 inline h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
)}
|
||||||
<p className="whitespace-pre-wrap">{msg.content}</p>
|
<p className="whitespace-pre-wrap">{msg.content}</p>
|
||||||
<p className={cn(
|
<p
|
||||||
'mt-1 text-[10px]',
|
className={cn(
|
||||||
msg.role === 'user' ? 'text-primary-foreground/60' : 'text-muted-foreground'
|
"mt-1 text-[10px]",
|
||||||
)}>
|
msg.role === "user"
|
||||||
{new Date(msg.timestamp).toLocaleTimeString('ro-RO', { hour: '2-digit', minute: '2-digit' })}
|
? "text-primary-foreground/60"
|
||||||
|
: "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{new Date(msg.timestamp).toLocaleTimeString("ro-RO", {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{sending && (
|
||||||
|
<div className="flex max-w-[80%] items-center gap-2 rounded-lg bg-muted px-3 py-2 text-sm text-muted-foreground">
|
||||||
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||||
|
<span>Se generează răspunsul...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div ref={messagesEndRef} />
|
<div ref={messagesEndRef} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -149,12 +324,15 @@ export function AiChatModule() {
|
|||||||
<Input
|
<Input
|
||||||
value={input}
|
value={input}
|
||||||
onChange={(e) => setInput(e.target.value)}
|
onChange={(e) => setInput(e.target.value)}
|
||||||
onKeyDown={(e) => e.key === 'Enter' && !e.shiftKey && handleSend()}
|
onKeyDown={(e) =>
|
||||||
placeholder="Scrie un mesaj..."
|
e.key === "Enter" && !e.shiftKey && handleSend()
|
||||||
|
}
|
||||||
|
placeholder={sending ? "Se generează..." : "Scrie un mesaj..."}
|
||||||
|
disabled={sending}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
<Button onClick={handleSend} disabled={!input.trim()}>
|
<Button onClick={handleSend} disabled={!input.trim() || sending}>
|
||||||
<Send className="h-4 w-4" />
|
{sending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ import type { ModuleConfig } from '@/core/module-registry/types';
|
|||||||
export const aiChatConfig: ModuleConfig = {
|
export const aiChatConfig: ModuleConfig = {
|
||||||
id: 'ai-chat',
|
id: 'ai-chat',
|
||||||
name: 'Chat AI',
|
name: 'Chat AI',
|
||||||
description: 'Interfață de conversație cu modele AI pentru asistență profesională',
|
description: 'Interfață de conversație cu modele AI (OpenAI, Claude, Ollama) pentru asistență profesională',
|
||||||
icon: 'message-square',
|
icon: 'message-square',
|
||||||
route: '/ai-chat',
|
route: '/ai-chat',
|
||||||
category: 'ai',
|
category: 'ai',
|
||||||
featureFlag: 'module.ai-chat',
|
featureFlag: 'module.ai-chat',
|
||||||
visibility: 'all',
|
visibility: 'all',
|
||||||
version: '0.1.0',
|
version: '0.2.0',
|
||||||
dependencies: [],
|
dependencies: [],
|
||||||
storageNamespace: 'ai-chat',
|
storageNamespace: 'ai-chat',
|
||||||
navOrder: 51,
|
navOrder: 51,
|
||||||
tags: ['chat', 'ai', 'conversație'],
|
tags: ['chat', 'ai', 'conversație', 'openai', 'claude', 'ollama'],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { useStorage } from "@/core/storage";
|
import { useStorage } from "@/core/storage";
|
||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
import type { ChatMessage, ChatSession } from "../types";
|
import type { ChatMessage, ChatSession, AiProviderConfig } from "../types";
|
||||||
|
|
||||||
const SESSION_PREFIX = "session:";
|
const SESSION_PREFIX = "session:";
|
||||||
|
|
||||||
@@ -12,6 +12,10 @@ export function useChat() {
|
|||||||
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
||||||
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [sending, setSending] = useState(false);
|
||||||
|
const [providerConfig, setProviderConfig] = useState<AiProviderConfig | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
const activeSession = sessions.find((s) => s.id === activeSessionId) ?? null;
|
const activeSession = sessions.find((s) => s.id === activeSessionId) ?? null;
|
||||||
|
|
||||||
@@ -29,18 +33,27 @@ export function useChat() {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, [storage]);
|
}, [storage]);
|
||||||
|
|
||||||
|
// Load sessions + check provider config
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refresh();
|
refresh();
|
||||||
|
fetch("/api/ai-chat")
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((data) => setProviderConfig(data as AiProviderConfig))
|
||||||
|
.catch(() => {
|
||||||
|
/* silent */
|
||||||
|
});
|
||||||
}, [refresh]);
|
}, [refresh]);
|
||||||
|
|
||||||
const createSession = useCallback(
|
const createSession = useCallback(
|
||||||
async (title?: string) => {
|
async (title?: string, projectTagId?: string, projectName?: string) => {
|
||||||
const session: ChatSession = {
|
const session: ChatSession = {
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
title: title || `Conversație ${new Date().toLocaleDateString("ro-RO")}`,
|
title: title || `Conversație ${new Date().toLocaleDateString("ro-RO")}`,
|
||||||
messages: [],
|
messages: [],
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
|
projectTagId,
|
||||||
|
projectName,
|
||||||
};
|
};
|
||||||
await storage.set(`${SESSION_PREFIX}${session.id}`, session);
|
await storage.set(`${SESSION_PREFIX}${session.id}`, session);
|
||||||
setSessions((prev) => [session, ...prev]);
|
setSessions((prev) => [session, ...prev]);
|
||||||
@@ -50,6 +63,22 @@ export function useChat() {
|
|||||||
[storage],
|
[storage],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const updateSession = useCallback(
|
||||||
|
async (
|
||||||
|
id: string,
|
||||||
|
updates: Partial<
|
||||||
|
Pick<ChatSession, "title" | "projectTagId" | "projectName">
|
||||||
|
>,
|
||||||
|
) => {
|
||||||
|
const current = sessions.find((s) => s.id === id);
|
||||||
|
if (!current) return;
|
||||||
|
const updated = { ...current, ...updates };
|
||||||
|
await storage.set(`${SESSION_PREFIX}${id}`, updated);
|
||||||
|
setSessions((prev) => prev.map((s) => (s.id === id ? updated : s)));
|
||||||
|
},
|
||||||
|
[storage, sessions],
|
||||||
|
);
|
||||||
|
|
||||||
const addMessage = useCallback(
|
const addMessage = useCallback(
|
||||||
async (content: string, role: ChatMessage["role"]) => {
|
async (content: string, role: ChatMessage["role"]) => {
|
||||||
if (!activeSessionId) return;
|
if (!activeSessionId) return;
|
||||||
@@ -79,6 +108,66 @@ export function useChat() {
|
|||||||
[storage, activeSessionId, sessions],
|
[storage, activeSessionId, sessions],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a user message and get AI response from /api/ai-chat.
|
||||||
|
* Falls back to demo mode if provider is not configured.
|
||||||
|
*/
|
||||||
|
const sendMessage = useCallback(
|
||||||
|
async (content: string) => {
|
||||||
|
if (!activeSessionId || sending) return;
|
||||||
|
|
||||||
|
// Add user message first
|
||||||
|
await addMessage(content, "user");
|
||||||
|
setSending(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Build message history for API from current + the new message
|
||||||
|
const current = sessions.find((s) => s.id === activeSessionId);
|
||||||
|
const apiMessages = [
|
||||||
|
...(current?.messages ?? []).map((m) => ({
|
||||||
|
role: m.role as "user" | "assistant",
|
||||||
|
content: m.content,
|
||||||
|
})),
|
||||||
|
{ role: "user" as const, content },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add project context to system prompt if linked
|
||||||
|
let systemPrompt: string | undefined;
|
||||||
|
if (current?.projectName) {
|
||||||
|
systemPrompt = `Ești un asistent AI pentru biroul de arhitectură. Contextul conversației: proiectul "${current.projectName}". Răspunzi în limba română cu terminologie profesională de arhitectură, urbanism și construcții.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch("/api/ai-chat", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ messages: apiMessages, systemPrompt }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = (await res.json().catch(() => ({}))) as {
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
await addMessage(
|
||||||
|
`Eroare API: ${err.message ?? `HTTP ${res.status}`}`,
|
||||||
|
"assistant",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await res.json()) as { content: string };
|
||||||
|
await addMessage(data.content, "assistant");
|
||||||
|
} catch (error) {
|
||||||
|
await addMessage(
|
||||||
|
`Eroare de conexiune: ${error instanceof Error ? error.message : "necunoscută"}`,
|
||||||
|
"assistant",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setSending(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[activeSessionId, sessions, sending, addMessage],
|
||||||
|
);
|
||||||
|
|
||||||
const deleteSession = useCallback(
|
const deleteSession = useCallback(
|
||||||
async (id: string) => {
|
async (id: string) => {
|
||||||
await storage.delete(`${SESSION_PREFIX}${id}`);
|
await storage.delete(`${SESSION_PREFIX}${id}`);
|
||||||
@@ -99,7 +188,11 @@ export function useChat() {
|
|||||||
activeSession,
|
activeSession,
|
||||||
activeSessionId,
|
activeSessionId,
|
||||||
loading,
|
loading,
|
||||||
|
sending,
|
||||||
|
providerConfig,
|
||||||
createSession,
|
createSession,
|
||||||
|
updateSession,
|
||||||
|
sendMessage,
|
||||||
addMessage,
|
addMessage,
|
||||||
deleteSession,
|
deleteSession,
|
||||||
selectSession,
|
selectSession,
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export { aiChatConfig } from './config';
|
export { aiChatConfig } from './config';
|
||||||
export { AiChatModule } from './components/ai-chat-module';
|
export { AiChatModule } from './components/ai-chat-module';
|
||||||
export type { ChatMessage, ChatRole, ChatSession } from './types';
|
export type { ChatMessage, ChatRole, ChatSession, AiProviderConfig } from './types';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export type ChatRole = 'user' | 'assistant';
|
export type ChatRole = "user" | "assistant";
|
||||||
|
|
||||||
export interface ChatMessage {
|
export interface ChatMessage {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -12,4 +12,16 @@ export interface ChatSession {
|
|||||||
title: string;
|
title: string;
|
||||||
messages: ChatMessage[];
|
messages: ChatMessage[];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
/** Linked project tag id from Tag Manager */
|
||||||
|
projectTagId?: string;
|
||||||
|
/** Project display name for quick reference */
|
||||||
|
projectName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiProviderConfig {
|
||||||
|
provider: string;
|
||||||
|
model: string;
|
||||||
|
baseUrl: string;
|
||||||
|
maxTokens: number;
|
||||||
|
isConfigured: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +1,87 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import { ArrowLeft, Copy, Check, Save, Trash2, History, Sparkles } from 'lucide-react';
|
import {
|
||||||
import { Button } from '@/shared/components/ui/button';
|
ArrowLeft,
|
||||||
import { Input } from '@/shared/components/ui/input';
|
Copy,
|
||||||
import { Label } from '@/shared/components/ui/label';
|
Check,
|
||||||
import { Textarea } from '@/shared/components/ui/textarea';
|
Save,
|
||||||
import { Badge } from '@/shared/components/ui/badge';
|
Trash2,
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/shared/components/ui/card';
|
History,
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
|
Sparkles,
|
||||||
import { Separator } from '@/shared/components/ui/separator';
|
Search,
|
||||||
import type { PromptTemplate, PromptVariable } from '../types';
|
Image,
|
||||||
import { usePromptGenerator } from '../hooks/use-prompt-generator';
|
} from "lucide-react";
|
||||||
import { cn } from '@/shared/lib/utils';
|
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> = {
|
const CATEGORY_LABELS: Record<string, string> = {
|
||||||
architecture: 'Arhitectură',
|
architecture: "Arhitectură",
|
||||||
legal: 'Legal',
|
legal: "Legal",
|
||||||
technical: 'Tehnic',
|
technical: "Tehnic",
|
||||||
administrative: 'Administrativ',
|
administrative: "Administrativ",
|
||||||
gis: 'GIS',
|
gis: "GIS",
|
||||||
bim: 'BIM',
|
bim: "BIM",
|
||||||
rendering: 'Vizualizare',
|
rendering: "Vizualizare",
|
||||||
procurement: 'Achiziții',
|
procurement: "Achiziții",
|
||||||
general: 'General',
|
general: "General",
|
||||||
};
|
};
|
||||||
|
|
||||||
const TARGET_LABELS: Record<string, string> = {
|
const TARGET_LABELS: Record<string, string> = {
|
||||||
text: 'Text', image: 'Imagine', code: 'Cod', review: 'Review', rewrite: 'Rescriere',
|
text: "Text",
|
||||||
|
image: "Imagine",
|
||||||
|
code: "Cod",
|
||||||
|
review: "Review",
|
||||||
|
rewrite: "Rescriere",
|
||||||
};
|
};
|
||||||
|
|
||||||
type ViewMode = 'templates' | 'compose' | 'history';
|
type ViewMode = "templates" | "compose" | "history";
|
||||||
|
|
||||||
export function PromptGeneratorModule() {
|
export function PromptGeneratorModule() {
|
||||||
const {
|
const {
|
||||||
allTemplates, selectedTemplate, values, composedPrompt,
|
allTemplates,
|
||||||
history, selectTemplate, updateValue, saveToHistory,
|
selectedTemplate,
|
||||||
deleteHistoryEntry, clearSelection,
|
values,
|
||||||
|
composedPrompt,
|
||||||
|
history,
|
||||||
|
selectTemplate,
|
||||||
|
updateValue,
|
||||||
|
saveToHistory,
|
||||||
|
deleteHistoryEntry,
|
||||||
|
clearSelection,
|
||||||
} = usePromptGenerator();
|
} = usePromptGenerator();
|
||||||
|
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>('templates');
|
const [viewMode, setViewMode] = useState<ViewMode>("templates");
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [saved, setSaved] = useState(false);
|
const [saved, setSaved] = useState(false);
|
||||||
const [filterCategory, setFilterCategory] = useState<string>('all');
|
const [filterCategory, setFilterCategory] = useState<string>("all");
|
||||||
|
const [filterTarget, setFilterTarget] = useState<string>("all");
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
const handleSelectTemplate = (template: PromptTemplate) => {
|
const handleSelectTemplate = (template: PromptTemplate) => {
|
||||||
selectTemplate(template);
|
selectTemplate(template);
|
||||||
setViewMode('compose');
|
setViewMode("compose");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopy = async () => {
|
const handleCopy = async () => {
|
||||||
@@ -54,7 +89,9 @@ export function PromptGeneratorModule() {
|
|||||||
await navigator.clipboard.writeText(composedPrompt);
|
await navigator.clipboard.writeText(composedPrompt);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
setTimeout(() => setCopied(false), 2000);
|
setTimeout(() => setCopied(false), 2000);
|
||||||
} catch { /* silent */ }
|
} catch {
|
||||||
|
/* silent */
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
@@ -65,51 +102,108 @@ export function PromptGeneratorModule() {
|
|||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
clearSelection();
|
clearSelection();
|
||||||
setViewMode('templates');
|
setViewMode("templates");
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredTemplates = filterCategory === 'all'
|
const filteredTemplates = allTemplates.filter((t) => {
|
||||||
? allTemplates
|
if (filterCategory !== "all" && t.category !== filterCategory) return false;
|
||||||
: allTemplates.filter((t) => t.category === filterCategory);
|
if (filterTarget !== "all" && t.targetAiType !== filterTarget) return false;
|
||||||
|
if (searchQuery) {
|
||||||
|
const q = searchQuery.toLowerCase();
|
||||||
|
return (
|
||||||
|
t.name.toLowerCase().includes(q) ||
|
||||||
|
t.description.toLowerCase().includes(q) ||
|
||||||
|
t.tags.some((tag) => tag.toLowerCase().includes(q)) ||
|
||||||
|
(CATEGORY_LABELS[t.category] ?? "").toLowerCase().includes(q)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
const usedCategories = [...new Set(allTemplates.map((t) => t.category))];
|
const usedCategories = [...new Set(allTemplates.map((t) => t.category))];
|
||||||
|
const usedTargets = [...new Set(allTemplates.map((t) => t.targetAiType))];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Navigation */}
|
{/* Navigation */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant={viewMode === 'templates' ? 'default' : 'outline'}
|
variant={viewMode === "templates" ? "default" : "outline"}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => { clearSelection(); setViewMode('templates'); }}
|
onClick={() => {
|
||||||
|
clearSelection();
|
||||||
|
setViewMode("templates");
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Sparkles className="mr-1 h-3.5 w-3.5" /> Șabloane
|
<Sparkles className="mr-1 h-3.5 w-3.5" /> Șabloane
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={viewMode === 'history' ? 'default' : 'outline'}
|
variant={viewMode === "history" ? "default" : "outline"}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setViewMode('history')}
|
onClick={() => setViewMode("history")}
|
||||||
>
|
>
|
||||||
<History className="mr-1 h-3.5 w-3.5" /> Istoric ({history.length})
|
<History className="mr-1 h-3.5 w-3.5" /> Istoric ({history.length})
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Template browser */}
|
{/* Template browser */}
|
||||||
{viewMode === 'templates' && (
|
{viewMode === "templates" && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<Label>Categorie:</Label>
|
<div className="relative min-w-[200px] flex-1">
|
||||||
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="Caută șabloane..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="pl-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<Select value={filterCategory} onValueChange={setFilterCategory}>
|
<Select value={filterCategory} onValueChange={setFilterCategory}>
|
||||||
<SelectTrigger className="w-[180px]"><SelectValue /></SelectTrigger>
|
<SelectTrigger className="w-[160px]">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">Toate</SelectItem>
|
<SelectItem value="all">Toate categoriile</SelectItem>
|
||||||
{usedCategories.map((cat) => (
|
{usedCategories.map((cat) => (
|
||||||
<SelectItem key={cat} value={cat}>{CATEGORY_LABELS[cat] ?? cat}</SelectItem>
|
<SelectItem key={cat} value={cat}>
|
||||||
|
{CATEGORY_LABELS[cat] ?? cat}
|
||||||
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
<Select value={filterTarget} onValueChange={setFilterTarget}>
|
||||||
|
<SelectTrigger className="w-[140px]">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Toate tipurile</SelectItem>
|
||||||
|
{usedTargets.map((t) => (
|
||||||
|
<SelectItem key={t} value={t}>
|
||||||
|
{TARGET_LABELS[t] ?? t}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{allTemplates.filter((t) => t.targetAiType === "image").length >
|
||||||
|
0 && (
|
||||||
|
<Button
|
||||||
|
variant={filterTarget === "image" ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
setFilterTarget(filterTarget === "image" ? "all" : "image")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Image className="mr-1 h-3.5 w-3.5" /> Imagine
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{filteredTemplates.length === 0 ? (
|
||||||
|
<p className="py-8 text-center text-sm text-muted-foreground">
|
||||||
|
Niciun șablon găsit pentru “{searchQuery}”.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
<div className="grid gap-3 sm:grid-cols-2">
|
<div className="grid gap-3 sm:grid-cols-2">
|
||||||
{filteredTemplates.map((template) => (
|
{filteredTemplates.map((template) => (
|
||||||
<Card
|
<Card
|
||||||
@@ -120,25 +214,37 @@ export function PromptGeneratorModule() {
|
|||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<CardTitle className="text-sm">{template.name}</CardTitle>
|
<CardTitle className="text-sm">{template.name}</CardTitle>
|
||||||
<Badge variant="outline" className="text-[10px]">{TARGET_LABELS[template.targetAiType]}</Badge>
|
<Badge variant="outline" className="text-[10px]">
|
||||||
|
{TARGET_LABELS[template.targetAiType]}
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<CardDescription className="text-xs">{template.description}</CardDescription>
|
<CardDescription className="text-xs">
|
||||||
|
{template.description}
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-0">
|
<CardContent className="pt-0">
|
||||||
<div className="flex flex-wrap gap-1">
|
<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]">
|
||||||
<Badge variant="secondary" className="text-[10px]">{template.variables.length} variabile</Badge>
|
{CATEGORY_LABELS[template.category] ??
|
||||||
<Badge variant="secondary" className="text-[10px]">{template.blocks.length} blocuri</Badge>
|
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>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Composition view */}
|
{/* Composition view */}
|
||||||
{viewMode === 'compose' && selectedTemplate && (
|
{viewMode === "compose" && selectedTemplate && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Button variant="ghost" size="sm" onClick={handleBack}>
|
<Button variant="ghost" size="sm" onClick={handleBack}>
|
||||||
@@ -146,14 +252,18 @@ export function PromptGeneratorModule() {
|
|||||||
</Button>
|
</Button>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold">{selectedTemplate.name}</h3>
|
<h3 className="font-semibold">{selectedTemplate.name}</h3>
|
||||||
<p className="text-xs text-muted-foreground">{selectedTemplate.description}</p>
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{selectedTemplate.description}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-6 lg:grid-cols-2">
|
<div className="grid gap-6 lg:grid-cols-2">
|
||||||
{/* Variable form */}
|
{/* Variable form */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader><CardTitle className="text-base">Variabile</CardTitle></CardHeader>
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Variabile</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
{selectedTemplate.variables.map((variable) => (
|
{selectedTemplate.variables.map((variable) => (
|
||||||
<VariableField
|
<VariableField
|
||||||
@@ -171,23 +281,46 @@ export function PromptGeneratorModule() {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-sm font-semibold">Prompt compus</h3>
|
<h3 className="text-sm font-semibold">Prompt compus</h3>
|
||||||
<div className="flex gap-1.5">
|
<div className="flex gap-1.5">
|
||||||
<Button variant="outline" size="sm" onClick={handleSave} disabled={!composedPrompt}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={!composedPrompt}
|
||||||
|
>
|
||||||
<Save className="mr-1 h-3.5 w-3.5" />
|
<Save className="mr-1 h-3.5 w-3.5" />
|
||||||
{saved ? 'Salvat!' : 'Salvează'}
|
{saved ? "Salvat!" : "Salvează"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" onClick={handleCopy} disabled={!composedPrompt}>
|
<Button
|
||||||
{copied ? <Check className="mr-1 h-3.5 w-3.5" /> : <Copy className="mr-1 h-3.5 w-3.5" />}
|
size="sm"
|
||||||
{copied ? 'Copiat!' : 'Copiază'}
|
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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-h-[300px] whitespace-pre-wrap rounded-lg border bg-muted/30 p-4 text-sm">
|
<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>}
|
{composedPrompt || (
|
||||||
|
<span className="text-muted-foreground italic">
|
||||||
|
Completează variabilele pentru a genera promptul...
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
<Badge variant="outline" className="text-[10px]">Output: {selectedTemplate.outputMode}</Badge>
|
<Badge variant="outline" className="text-[10px]">
|
||||||
<Badge variant="outline" className="text-[10px]">Blocuri: {selectedTemplate.blocks.length}</Badge>
|
Output: {selectedTemplate.outputMode}
|
||||||
<Badge variant="outline" className="text-[10px]">Caractere: {composedPrompt.length}</Badge>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
@@ -195,10 +328,12 @@ export function PromptGeneratorModule() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* History view */}
|
{/* History view */}
|
||||||
{viewMode === 'history' && (
|
{viewMode === "history" && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{history.length === 0 ? (
|
{history.length === 0 ? (
|
||||||
<p className="py-8 text-center text-sm text-muted-foreground">Niciun prompt salvat în istoric.</p>
|
<p className="py-8 text-center text-sm text-muted-foreground">
|
||||||
|
Niciun prompt salvat în istoric.
|
||||||
|
</p>
|
||||||
) : (
|
) : (
|
||||||
history.map((entry) => (
|
history.map((entry) => (
|
||||||
<Card key={entry.id} className="group">
|
<Card key={entry.id} className="group">
|
||||||
@@ -206,11 +341,16 @@ export function PromptGeneratorModule() {
|
|||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<p className="text-sm font-medium">{entry.templateName}</p>
|
<p className="text-sm font-medium">
|
||||||
<Badge variant="outline" className="text-[10px]">{entry.outputMode}</Badge>
|
{entry.templateName}
|
||||||
|
</p>
|
||||||
|
<Badge variant="outline" className="text-[10px]">
|
||||||
|
{entry.outputMode}
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
{new Date(entry.createdAt).toLocaleString('ro-RO')} — {entry.composedPrompt.length} caractere
|
{new Date(entry.createdAt).toLocaleString("ro-RO")} —{" "}
|
||||||
|
{entry.composedPrompt.length} caractere
|
||||||
</p>
|
</p>
|
||||||
<pre className="mt-2 max-h-24 overflow-hidden truncate text-xs text-muted-foreground">
|
<pre className="mt-2 max-h-24 overflow-hidden truncate text-xs text-muted-foreground">
|
||||||
{entry.composedPrompt.slice(0, 200)}...
|
{entry.composedPrompt.slice(0, 200)}...
|
||||||
@@ -218,7 +358,12 @@ export function PromptGeneratorModule() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
<div className="flex gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
||||||
<CopyHistoryButton text={entry.composedPrompt} />
|
<CopyHistoryButton text={entry.composedPrompt} />
|
||||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-destructive" onClick={() => deleteHistoryEntry(entry.id)}>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-7 w-7 text-destructive"
|
||||||
|
onClick={() => deleteHistoryEntry(entry.id)}
|
||||||
|
>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -233,44 +378,68 @@ export function PromptGeneratorModule() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function VariableField({ variable, value, onChange }: {
|
function VariableField({
|
||||||
|
variable,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
variable: PromptVariable;
|
variable: PromptVariable;
|
||||||
value: unknown;
|
value: unknown;
|
||||||
onChange: (val: unknown) => void;
|
onChange: (val: unknown) => void;
|
||||||
}) {
|
}) {
|
||||||
const strVal = value !== undefined && value !== null ? String(value) : '';
|
const strVal = value !== undefined && value !== null ? String(value) : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Label className={cn(variable.required && 'after:content-["*"] after:ml-0.5 after:text-destructive')}>
|
<Label
|
||||||
|
className={cn(
|
||||||
|
variable.required &&
|
||||||
|
'after:content-["*"] after:ml-0.5 after:text-destructive',
|
||||||
|
)}
|
||||||
|
>
|
||||||
{variable.label}
|
{variable.label}
|
||||||
</Label>
|
</Label>
|
||||||
{variable.helperText && (
|
{variable.helperText && (
|
||||||
<p className="text-[11px] text-muted-foreground">{variable.helperText}</p>
|
<p className="text-[11px] text-muted-foreground">
|
||||||
|
{variable.helperText}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(variable.type === 'text' || variable.type === 'number') && (
|
{(variable.type === "text" || variable.type === "number") && (
|
||||||
<Input
|
<Input
|
||||||
type={variable.type === 'number' ? 'number' : 'text'}
|
type={variable.type === "number" ? "number" : "text"}
|
||||||
value={strVal}
|
value={strVal}
|
||||||
onChange={(e) => onChange(variable.type === 'number' ? Number(e.target.value) : e.target.value)}
|
onChange={(e) =>
|
||||||
|
onChange(
|
||||||
|
variable.type === "number"
|
||||||
|
? Number(e.target.value)
|
||||||
|
: e.target.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
placeholder={variable.placeholder}
|
placeholder={variable.placeholder}
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(variable.type === 'select' || variable.type === 'tone-selector' || variable.type === 'company-selector') && variable.options && (
|
{(variable.type === "select" ||
|
||||||
|
variable.type === "tone-selector" ||
|
||||||
|
variable.type === "company-selector") &&
|
||||||
|
variable.options && (
|
||||||
<Select value={strVal} onValueChange={onChange}>
|
<Select value={strVal} onValueChange={onChange}>
|
||||||
<SelectTrigger className="mt-1"><SelectValue placeholder="Selectează..." /></SelectTrigger>
|
<SelectTrigger className="mt-1">
|
||||||
|
<SelectValue placeholder="Selectează..." />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{variable.options.map((opt) => (
|
{variable.options.map((opt) => (
|
||||||
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
|
<SelectItem key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{variable.type === 'boolean' && (
|
{variable.type === "boolean" && (
|
||||||
<div className="mt-1 flex items-center gap-2">
|
<div className="mt-1 flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -278,26 +447,33 @@ function VariableField({ variable, value, onChange }: {
|
|||||||
onChange={(e) => onChange(e.target.checked)}
|
onChange={(e) => onChange(e.target.checked)}
|
||||||
className="h-4 w-4 rounded accent-primary"
|
className="h-4 w-4 rounded accent-primary"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-muted-foreground">{variable.placeholder ?? 'Da/Nu'}</span>
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{variable.placeholder ?? "Da/Nu"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{variable.type === 'multi-select' && variable.options && (
|
{variable.type === "multi-select" && variable.options && (
|
||||||
<div className="mt-1 flex flex-wrap gap-1.5">
|
<div className="mt-1 flex flex-wrap gap-1.5">
|
||||||
{variable.options.map((opt) => {
|
{variable.options.map((opt) => {
|
||||||
const selected = Array.isArray(value) && (value as string[]).includes(opt.value);
|
const selected =
|
||||||
|
Array.isArray(value) && (value as string[]).includes(opt.value);
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={opt.value}
|
key={opt.value}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const arr = Array.isArray(value) ? [...(value as string[])] : [];
|
const arr = Array.isArray(value)
|
||||||
|
? [...(value as string[])]
|
||||||
|
: [];
|
||||||
if (selected) onChange(arr.filter((v) => v !== opt.value));
|
if (selected) onChange(arr.filter((v) => v !== opt.value));
|
||||||
else onChange([...arr, opt.value]);
|
else onChange([...arr, opt.value]);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-full border px-2.5 py-0.5 text-xs transition-colors',
|
"rounded-full border px-2.5 py-0.5 text-xs transition-colors",
|
||||||
selected ? 'border-primary bg-primary text-primary-foreground' : 'hover:bg-accent'
|
selected
|
||||||
|
? "border-primary bg-primary text-primary-foreground"
|
||||||
|
: "hover:bg-accent",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{opt.label}
|
{opt.label}
|
||||||
@@ -317,11 +493,22 @@ function CopyHistoryButton({ text }: { text: string }) {
|
|||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
setTimeout(() => setCopied(false), 1500);
|
setTimeout(() => setCopied(false), 1500);
|
||||||
} catch { /* silent */ }
|
} catch {
|
||||||
|
/* silent */
|
||||||
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={handleCopy}>
|
<Button
|
||||||
{copied ? <Check className="h-3.5 w-3.5 text-green-500" /> : <Copy className="h-3.5 w-3.5" />}
|
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>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ import type { ModuleConfig } from '@/core/module-registry/types';
|
|||||||
export const promptGeneratorConfig: ModuleConfig = {
|
export const promptGeneratorConfig: ModuleConfig = {
|
||||||
id: 'prompt-generator',
|
id: 'prompt-generator',
|
||||||
name: 'Generator Prompturi',
|
name: 'Generator Prompturi',
|
||||||
description: 'Generator structurat de prompturi pe bază de șabloane parametrizate, organizate pe domenii profesionale',
|
description: 'Generator structurat de prompturi pe bază de șabloane parametrizate, cu căutare și filtrare pe domenii profesionale și tipuri AI',
|
||||||
icon: 'sparkles',
|
icon: 'sparkles',
|
||||||
route: '/prompt-generator',
|
route: '/prompt-generator',
|
||||||
category: 'ai',
|
category: 'ai',
|
||||||
featureFlag: 'module.prompt-generator',
|
featureFlag: 'module.prompt-generator',
|
||||||
visibility: 'all',
|
visibility: 'all',
|
||||||
version: '0.1.0',
|
version: '0.2.0',
|
||||||
dependencies: [],
|
dependencies: [],
|
||||||
storageNamespace: 'prompt-generator',
|
storageNamespace: 'prompt-generator',
|
||||||
navOrder: 50,
|
navOrder: 50,
|
||||||
tags: ['prompt', 'ai', 'generator', 'șabloane'],
|
tags: ['prompt', 'ai', 'generator', 'șabloane', 'midjourney', 'stable-diffusion', 'imagine'],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1839,4 +1839,627 @@ export const BUILTIN_TEMPLATES: PromptTemplate[] = [
|
|||||||
author: "ArchiTools",
|
author: "ArchiTools",
|
||||||
visibility: "all",
|
visibility: "all",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── Image Generation Templates (Midjourney / Stable Diffusion) ──
|
||||||
|
|
||||||
|
{
|
||||||
|
id: "midjourney-exterior",
|
||||||
|
name: "Randare Exterioară Midjourney",
|
||||||
|
category: "rendering",
|
||||||
|
domain: "architecture-visualization",
|
||||||
|
description:
|
||||||
|
"Generează un prompt optimizat pentru Midjourney v6 — randare arhitecturală exterioară fotorealistă.",
|
||||||
|
targetAiType: "image",
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
id: "b1",
|
||||||
|
type: "task",
|
||||||
|
label: "Subiect principal",
|
||||||
|
content:
|
||||||
|
"{{buildingType}}, {{style}} architecture, {{materials}}, located in {{environment}}",
|
||||||
|
order: 1,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b2",
|
||||||
|
type: "context",
|
||||||
|
label: "Atmosferă și lumină",
|
||||||
|
content:
|
||||||
|
"{{lighting}} lighting, {{timeOfDay}}, {{weather}}, {{season}}",
|
||||||
|
order: 2,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b3",
|
||||||
|
type: "format",
|
||||||
|
label: "Parametri tehnici",
|
||||||
|
content:
|
||||||
|
"architectural photography, {{cameraAngle}}, {{lensType}}, high detail, 8k, photorealistic --ar {{aspectRatio}} --v 6 --style raw --s {{stylize}}",
|
||||||
|
order: 3,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b4",
|
||||||
|
type: "constraints",
|
||||||
|
label: "Constrângeri negative",
|
||||||
|
content: "{{negativePrompt}}",
|
||||||
|
order: 4,
|
||||||
|
required: false,
|
||||||
|
conditional: {
|
||||||
|
variableId: "negativePrompt",
|
||||||
|
operator: "truthy",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
variables: [
|
||||||
|
{
|
||||||
|
id: "buildingType",
|
||||||
|
label: "Tip clădire",
|
||||||
|
type: "select",
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "modern residential house",
|
||||||
|
label: "Casă rezidențială modernă",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "contemporary apartment building",
|
||||||
|
label: "Bloc apartamente contemporan",
|
||||||
|
},
|
||||||
|
{ value: "commercial office building", label: "Clădire birouri" },
|
||||||
|
{ value: "cultural center", label: "Centru cultural" },
|
||||||
|
{ value: "mixed-use development", label: "Complex multifuncțional" },
|
||||||
|
{ value: "villa with pool", label: "Vilă cu piscină" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "style",
|
||||||
|
label: "Stil arhitectural",
|
||||||
|
type: "select",
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ value: "minimalist", label: "Minimalist" },
|
||||||
|
{ value: "brutalist", label: "Brutalist" },
|
||||||
|
{ value: "parametric", label: "Parametric" },
|
||||||
|
{ value: "organic", label: "Organic" },
|
||||||
|
{ value: "neo-traditional", label: "Neo-tradițional" },
|
||||||
|
{ value: "high-tech", label: "High-tech" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "materials",
|
||||||
|
label: "Materiale",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
placeholder: "ex: exposed concrete, timber cladding, glass facade",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "environment",
|
||||||
|
label: "Mediu înconjurător",
|
||||||
|
type: "select",
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ value: "urban context with trees", label: "Urban cu arbori" },
|
||||||
|
{ value: "hillside with panoramic view", label: "Versant panoramic" },
|
||||||
|
{ value: "waterfront", label: "Lângă apă" },
|
||||||
|
{ value: "forest clearing", label: "Poiană în pădure" },
|
||||||
|
{ value: "dense city center", label: "Centru urban dens" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "lighting",
|
||||||
|
label: "Iluminare",
|
||||||
|
type: "select",
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ value: "golden hour", label: "Golden hour" },
|
||||||
|
{ value: "soft diffused", label: "Difuză" },
|
||||||
|
{ value: "dramatic", label: "Dramatică" },
|
||||||
|
{ value: "overcast ambient", label: "Ambient înnorat" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "timeOfDay",
|
||||||
|
label: "Moment al zilei",
|
||||||
|
type: "select",
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ value: "early morning", label: "Dimineață devreme" },
|
||||||
|
{ value: "midday", label: "Amiază" },
|
||||||
|
{ value: "late afternoon", label: "Seară" },
|
||||||
|
{ value: "blue hour dusk", label: "Blue hour / amurg" },
|
||||||
|
{
|
||||||
|
value: "night with interior lights",
|
||||||
|
label: "Noapte cu lumini interioare",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "weather",
|
||||||
|
label: "Vreme",
|
||||||
|
type: "select",
|
||||||
|
required: false,
|
||||||
|
options: [
|
||||||
|
{ value: "clear sky", label: "Cer senin" },
|
||||||
|
{ value: "partly cloudy", label: "Parțial înnorat" },
|
||||||
|
{ value: "light rain", label: "Ploaie ușoară" },
|
||||||
|
{ value: "snow covered", label: "Acoperit de zăpadă" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "season",
|
||||||
|
label: "Anotimp",
|
||||||
|
type: "select",
|
||||||
|
required: false,
|
||||||
|
options: [
|
||||||
|
{ value: "spring with blossoms", label: "Primăvară" },
|
||||||
|
{ value: "lush summer", label: "Vară" },
|
||||||
|
{ value: "autumn colors", label: "Toamnă" },
|
||||||
|
{ value: "winter", label: "Iarnă" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cameraAngle",
|
||||||
|
label: "Unghi cameră",
|
||||||
|
type: "select",
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ value: "eye-level perspective", label: "Nivel ochiului" },
|
||||||
|
{ value: "low angle dramatic", label: "Unghi jos dramatic" },
|
||||||
|
{ value: "aerial bird's eye", label: "Aerian" },
|
||||||
|
{ value: "three-quarter view", label: "Perspectivă 3/4" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "lensType",
|
||||||
|
label: "Tip obiectiv",
|
||||||
|
type: "select",
|
||||||
|
required: false,
|
||||||
|
options: [
|
||||||
|
{ value: "wide angle 24mm", label: "Wide 24mm" },
|
||||||
|
{ value: "standard 50mm", label: "Standard 50mm" },
|
||||||
|
{ value: "telephoto 85mm", label: "Telephoto 85mm" },
|
||||||
|
{ value: "tilt-shift", label: "Tilt-shift" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "aspectRatio",
|
||||||
|
label: "Aspect ratio",
|
||||||
|
type: "select",
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ value: "16:9", label: "16:9 (landscape)" },
|
||||||
|
{ value: "4:3", label: "4:3" },
|
||||||
|
{ value: "1:1", label: "1:1 (square)" },
|
||||||
|
{ value: "9:16", label: "9:16 (portrait)" },
|
||||||
|
{ value: "3:2", label: "3:2" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "stylize",
|
||||||
|
label: "Stylize (0-1000)",
|
||||||
|
type: "number",
|
||||||
|
required: false,
|
||||||
|
defaultValue: 250,
|
||||||
|
placeholder: "250",
|
||||||
|
helperText: "Valori mici = mai fidel textului, mari = mai artistic",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "negativePrompt",
|
||||||
|
label: "Prompt negativ (opțional)",
|
||||||
|
type: "text",
|
||||||
|
required: false,
|
||||||
|
placeholder: "ex: --no people, cars, text, watermark",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputMode: "short",
|
||||||
|
tags: ["midjourney", "exterior", "randare", "fotorealist", "arhitectură"],
|
||||||
|
version: "1.0.0",
|
||||||
|
author: "ArchiTools",
|
||||||
|
visibility: "all",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sd-interior-design",
|
||||||
|
name: "Design Interior — Stable Diffusion",
|
||||||
|
category: "rendering",
|
||||||
|
domain: "architecture-visualization",
|
||||||
|
description:
|
||||||
|
"Prompt optimizat pentru Stable Diffusion XL — vizualizare design interior cu control detaliat.",
|
||||||
|
targetAiType: "image",
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
id: "b1",
|
||||||
|
type: "task",
|
||||||
|
label: "Scenă",
|
||||||
|
content:
|
||||||
|
"interior design photograph, {{roomType}}, {{designStyle}} style, {{colorPalette}} color palette",
|
||||||
|
order: 1,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b2",
|
||||||
|
type: "context",
|
||||||
|
label: "Mobilier și detalii",
|
||||||
|
content:
|
||||||
|
"featuring {{furniture}}, {{flooring}} flooring, {{wallTreatment}} walls, {{decorElements}}",
|
||||||
|
order: 2,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b3",
|
||||||
|
type: "format",
|
||||||
|
label: "Calitate și stil",
|
||||||
|
content:
|
||||||
|
"professional interior photography, {{lightingMood}}, high resolution, 4k, detailed textures, editorial quality, architectural digest style",
|
||||||
|
order: 3,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
variables: [
|
||||||
|
{
|
||||||
|
id: "roomType",
|
||||||
|
label: "Tip cameră",
|
||||||
|
type: "select",
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ value: "spacious living room", label: "Living spațios" },
|
||||||
|
{ value: "modern kitchen", label: "Bucătărie modernă" },
|
||||||
|
{ value: "master bedroom", label: "Dormitor principal" },
|
||||||
|
{ value: "home office", label: "Birou acasă" },
|
||||||
|
{
|
||||||
|
value: "bathroom with natural stone",
|
||||||
|
label: "Baie cu piatră naturală",
|
||||||
|
},
|
||||||
|
{ value: "open-plan loft", label: "Loft open-plan" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "designStyle",
|
||||||
|
label: "Stil design",
|
||||||
|
type: "select",
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ value: "Scandinavian minimalist", label: "Scandinav minimalist" },
|
||||||
|
{ value: "Japanese wabi-sabi", label: "Wabi-sabi japonez" },
|
||||||
|
{ value: "industrial chic", label: "Industrial chic" },
|
||||||
|
{ value: "mid-century modern", label: "Mid-century modern" },
|
||||||
|
{ value: "Mediterranean warm", label: "Mediteranean cald" },
|
||||||
|
{ value: "contemporary luxury", label: "Lux contemporan" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "colorPalette",
|
||||||
|
label: "Paletă de culori",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
placeholder: "ex: warm neutrals with sage green accents",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "furniture",
|
||||||
|
label: "Mobilier principal",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
placeholder: "ex: modular sofa, oak dining table, pendant lights",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "flooring",
|
||||||
|
label: "Pardoseală",
|
||||||
|
type: "select",
|
||||||
|
required: false,
|
||||||
|
options: [
|
||||||
|
{ value: "hardwood oak", label: "Parchet stejar" },
|
||||||
|
{ value: "polished concrete", label: "Beton șlefuit" },
|
||||||
|
{ value: "marble", label: "Marmură" },
|
||||||
|
{ value: "terrazzo", label: "Terrazzo" },
|
||||||
|
{ value: "ceramic tile", label: "Gresie ceramică" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "wallTreatment",
|
||||||
|
label: "Finisaj pereți",
|
||||||
|
type: "select",
|
||||||
|
required: false,
|
||||||
|
options: [
|
||||||
|
{ value: "smooth plaster", label: "Tencuială netedă" },
|
||||||
|
{ value: "exposed brick", label: "Cărămidă aparentă" },
|
||||||
|
{ value: "wood panel", label: "Lambriu lemn" },
|
||||||
|
{ value: "textured wallpaper", label: "Tapet texturat" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "decorElements",
|
||||||
|
label: "Elemente decorative",
|
||||||
|
type: "text",
|
||||||
|
required: false,
|
||||||
|
placeholder: "ex: indoor plants, art prints, ceramic vases",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "lightingMood",
|
||||||
|
label: "Atmosferă luminoasă",
|
||||||
|
type: "select",
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "warm natural daylight streaming through large windows",
|
||||||
|
label: "Lumină naturală caldă",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "soft ambient evening light with warm lamps",
|
||||||
|
label: "Ambient seară cu lămpi calde",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "bright and airy with skylights",
|
||||||
|
label: "Luminos cu lucarnă",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "moody dramatic with accent lighting",
|
||||||
|
label: "Dramatic cu accente",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputMode: "short",
|
||||||
|
tags: ["stable diffusion", "interior", "design", "vizualizare"],
|
||||||
|
version: "1.0.0",
|
||||||
|
author: "ArchiTools",
|
||||||
|
visibility: "all",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "midjourney-infographic",
|
||||||
|
name: "Infografic Arhitectural Midjourney",
|
||||||
|
category: "rendering",
|
||||||
|
domain: "architecture-visualization",
|
||||||
|
description:
|
||||||
|
"Prompt pentru generarea de infografice, diagrame explicate și prezentări vizuale pentru proiecte de arhitectură.",
|
||||||
|
targetAiType: "image",
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
id: "b1",
|
||||||
|
type: "task",
|
||||||
|
label: "Subiect",
|
||||||
|
content:
|
||||||
|
"architectural {{infographicType}}, showing {{subject}}, {{visualStyle}} visual style",
|
||||||
|
order: 1,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b2",
|
||||||
|
type: "context",
|
||||||
|
label: "Conținut",
|
||||||
|
content:
|
||||||
|
"illustrating {{keyElements}}, with {{annotations}}, target audience: {{audience}}",
|
||||||
|
order: 2,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b3",
|
||||||
|
type: "format",
|
||||||
|
label: "Stil grafic",
|
||||||
|
content:
|
||||||
|
"clean vector style, {{colorScheme}}, professional infographic layout, high contrast, editorial quality --ar {{aspectRatio}} --v 6",
|
||||||
|
order: 3,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
variables: [
|
||||||
|
{
|
||||||
|
id: "infographicType",
|
||||||
|
label: "Tip infografic",
|
||||||
|
type: "select",
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "exploded axonometric diagram",
|
||||||
|
label: "Axonometrie explodată",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "section diagram with labels",
|
||||||
|
label: "Secțiune cu etichete",
|
||||||
|
},
|
||||||
|
{ value: "site analysis diagram", label: "Diagramă analiză sit" },
|
||||||
|
{ value: "material palette board", label: "Paletar materiale" },
|
||||||
|
{
|
||||||
|
value: "sustainability features diagram",
|
||||||
|
label: "Diagramă sustenabilitate",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "construction phases timeline",
|
||||||
|
label: "Timeline faze construcție",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "subject",
|
||||||
|
label: "Subiect",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
placeholder: "ex: passive house energy flow, structural system",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "visualStyle",
|
||||||
|
label: "Stil vizual",
|
||||||
|
type: "select",
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ value: "minimal flat", label: "Minimal flat" },
|
||||||
|
{ value: "isometric 3D", label: "Izometric 3D" },
|
||||||
|
{ value: "technical line drawing", label: "Desen tehnic" },
|
||||||
|
{ value: "watercolor sketch", label: "Schiță acuarelă" },
|
||||||
|
{ value: "collage mixed media", label: "Colaj mixed media" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "keyElements",
|
||||||
|
label: "Elemente cheie",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
placeholder: "ex: insulation layers, ventilation system, solar panels",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "annotations",
|
||||||
|
label: "Adnotări",
|
||||||
|
type: "select",
|
||||||
|
required: false,
|
||||||
|
options: [
|
||||||
|
{ value: "numbered callouts", label: "Callout-uri numerotate" },
|
||||||
|
{ value: "legend with icons", label: "Legendă cu pictograme" },
|
||||||
|
{ value: "dimension lines", label: "Cote dimensionale" },
|
||||||
|
{ value: "no annotations", label: "Fără adnotări" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "audience",
|
||||||
|
label: "Public țintă",
|
||||||
|
type: "select",
|
||||||
|
required: false,
|
||||||
|
options: [
|
||||||
|
{ value: "client presentation", label: "Prezentare client" },
|
||||||
|
{ value: "technical review", label: "Comisie tehnică" },
|
||||||
|
{ value: "public exhibition", label: "Expoziție publică" },
|
||||||
|
{ value: "academic portfolio", label: "Portofoliu academic" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "colorScheme",
|
||||||
|
label: "Schemă de culori",
|
||||||
|
type: "text",
|
||||||
|
required: false,
|
||||||
|
placeholder: "ex: muted earth tones, black and white with accent red",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "aspectRatio",
|
||||||
|
label: "Aspect ratio",
|
||||||
|
type: "select",
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ value: "16:9", label: "16:9 (landscape)" },
|
||||||
|
{ value: "1:1", label: "1:1 (square)" },
|
||||||
|
{ value: "9:16", label: "9:16 (poster)" },
|
||||||
|
{ value: "3:4", label: "3:4 (print)" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputMode: "short",
|
||||||
|
tags: ["midjourney", "infografic", "diagramă", "prezentare", "arhitectură"],
|
||||||
|
version: "1.0.0",
|
||||||
|
author: "ArchiTools",
|
||||||
|
visibility: "all",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sd-material-texture",
|
||||||
|
name: "Textură Material — Stable Diffusion",
|
||||||
|
category: "rendering",
|
||||||
|
domain: "architecture-visualization",
|
||||||
|
description:
|
||||||
|
"Generează texturi seamless pentru materiale de construcție — utilizare în SketchUp, 3ds Max, Blender.",
|
||||||
|
targetAiType: "image",
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
id: "b1",
|
||||||
|
type: "task",
|
||||||
|
label: "Material",
|
||||||
|
content:
|
||||||
|
"seamless tileable texture of {{material}}, {{surfaceFinish}} finish, {{patternType}} pattern",
|
||||||
|
order: 1,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b2",
|
||||||
|
type: "context",
|
||||||
|
label: "Detalii",
|
||||||
|
content: "{{aging}} aging, {{scale}} scale, viewed from {{viewAngle}}",
|
||||||
|
order: 2,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b3",
|
||||||
|
type: "format",
|
||||||
|
label: "Parametri tehnici",
|
||||||
|
content:
|
||||||
|
"PBR material texture, high resolution, 4k, seamless tiling, physically based rendering, no distortion, uniform lighting --ar 1:1 --tile",
|
||||||
|
order: 3,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
variables: [
|
||||||
|
{
|
||||||
|
id: "material",
|
||||||
|
label: "Material",
|
||||||
|
type: "select",
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ value: "natural oak wood", label: "Lemn stejar natural" },
|
||||||
|
{ value: "Carrara marble", label: "Marmură Carrara" },
|
||||||
|
{ value: "raw concrete", label: "Beton brut" },
|
||||||
|
{ value: "red brick", label: "Cărămidă roșie" },
|
||||||
|
{ value: "weathering steel corten", label: "Oțel Corten" },
|
||||||
|
{ value: "terracotta tiles", label: "Teracotă" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "surfaceFinish",
|
||||||
|
label: "Finisaj suprafață",
|
||||||
|
type: "select",
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ value: "matte", label: "Mat" },
|
||||||
|
{ value: "polished", label: "Lustruit" },
|
||||||
|
{ value: "rough", label: "Rugos" },
|
||||||
|
{ value: "brushed", label: "Periat" },
|
||||||
|
{ value: "sandblasted", label: "Sablat" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "patternType",
|
||||||
|
label: "Tip pattern",
|
||||||
|
type: "select",
|
||||||
|
required: false,
|
||||||
|
options: [
|
||||||
|
{ value: "natural grain", label: "Fibră naturală" },
|
||||||
|
{ value: "herringbone", label: "Herringbone" },
|
||||||
|
{ value: "running bond", label: "Running bond" },
|
||||||
|
{ value: "random mosaic", label: "Mozaic random" },
|
||||||
|
{ value: "linear", label: "Linear" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "aging",
|
||||||
|
label: "Uzură / Patină",
|
||||||
|
type: "select",
|
||||||
|
required: false,
|
||||||
|
options: [
|
||||||
|
{ value: "brand new pristine", label: "Nou" },
|
||||||
|
{ value: "slightly weathered", label: "Ușor patinat" },
|
||||||
|
{ value: "heavily worn vintage", label: "Foarte uzat / vintage" },
|
||||||
|
{ value: "moss and lichen covered", label: "Acoperit de mușchi" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "scale",
|
||||||
|
label: "Scală",
|
||||||
|
type: "select",
|
||||||
|
required: false,
|
||||||
|
options: [
|
||||||
|
{ value: "close-up macro", label: "Macro close-up" },
|
||||||
|
{ value: "medium 1:1", label: "Mediu 1:1" },
|
||||||
|
{ value: "wide repeating panel", label: "Panou larg repetitiv" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "viewAngle",
|
||||||
|
label: "Unghi vedere",
|
||||||
|
type: "select",
|
||||||
|
required: false,
|
||||||
|
options: [
|
||||||
|
{ value: "straight on orthographic", label: "Frontal ortografic" },
|
||||||
|
{ value: "slight angle", label: "Ușor înclinat" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputMode: "short",
|
||||||
|
tags: ["stable diffusion", "textură", "material", "PBR", "seamless"],
|
||||||
|
version: "1.0.0",
|
||||||
|
author: "ArchiTools",
|
||||||
|
visibility: "all",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user