diff --git a/src/controllers/document.controller.ts b/src/controllers/document.controller.ts index 492c247..f20c10d 100644 --- a/src/controllers/document.controller.ts +++ b/src/controllers/document.controller.ts @@ -68,7 +68,7 @@ async function callAiModel(systemPrompt: string, userText: string): Promise<{ re return { resultJson, aiModel }; } -async function callAiModelRaw(systemPrompt: string, userText: string): Promise<{ aiContent: string; aiModel: string }> { +export async function callAiModelRaw(systemPrompt: string, userText: string): Promise<{ aiContent: string; aiModel: string }> { const { runpodUrl, aiModel } = getAiConfig(); const response = await fetch(`${runpodUrl}/v1/chat/completions`, { @@ -168,6 +168,27 @@ export const registerLocalDocument = async (req: AuthenticatedRequest, res: Resp } }; +// uploadDocument ve uploadTemplate'in (template.controller.ts) ortak kullandığı +// metin çıkarma adımı: resimler için Tesseract OCR, ofis belgeleri için officeParser. +export async function extractTextFromUploadedFile(file: { buffer: Buffer; mimetype: string; originalname: string }): Promise { + const mimeType = file.mimetype.toLowerCase(); + + if (mimeType.includes('image/')) { + let imageBuffer = file.buffer; + if (mimeType === 'image/heic' || file.originalname.toLowerCase().endsWith('.heic')) { + imageBuffer = await heicConvert({ buffer: file.buffer, format: 'JPEG', quality: 1 }); + } + const { data: { text } } = await Tesseract.recognize(imageBuffer, 'tur'); + return text; + } + + // officeParser natively supports pdf, docx, doc, rtf, txt, etc. + // officeparser@7.x parseOffice() bir sonuc objesi donduruyor (duz string degil), + // duz metin icin .toText() cagirmak gerekiyor. + const result: any = await officeParser.parseOffice(file.buffer); + return typeof result === 'string' ? result : (result?.toText ? result.toText() : String(result || '')); +} + export const uploadDocument = async (req: AuthenticatedRequest, res: Response) => { try { const file = req.file; @@ -179,37 +200,11 @@ export const uploadDocument = async (req: AuthenticatedRequest, res: Response) = } let extractedText = ''; - const mimeType = file.mimetype.toLowerCase(); - - // IMAGE & OCR LOGIC - if (mimeType.includes('image/')) { - let imageBuffer = file.buffer; - - // Convert HEIC to JPEG - if (mimeType === 'image/heic' || file.originalname.toLowerCase().endsWith('.heic')) { - imageBuffer = await heicConvert({ - buffer: file.buffer, - format: 'JPEG', - quality: 1 - }); - } - - // Run Tesseract OCR (Turkish language) - const { data: { text } } = await Tesseract.recognize(imageBuffer, 'tur'); - extractedText = text; - - // DOCUMENT LOGIC - } else { - try { - // officeParser natively supports pdf, docx, doc, rtf, txt, etc. - // officeparser@7.x parseOffice() bir sonuc objesi donduruyor (duz string degil), - // duz metin icin .toText() cagirmak gerekiyor. - const result: any = await officeParser.parseOffice(file.buffer); - extractedText = typeof result === 'string' ? result : (result?.toText ? result.toText() : String(result || '')); - } catch (err: any) { - console.error('officeParser Error:', err); - return res.status(400).json({ error: 'Dosya formatı okunamadı veya desteklenmiyor.' }); - } + try { + extractedText = await extractTextFromUploadedFile(file); + } catch (err: any) { + console.error('Text Extraction Error:', err); + return res.status(400).json({ error: 'Dosya formatı okunamadı veya desteklenmiyor.' }); } // 2. Upload File to Supabase Storage diff --git a/src/controllers/drafting.controller.ts b/src/controllers/drafting.controller.ts new file mode 100644 index 0000000..a6a2a76 --- /dev/null +++ b/src/controllers/drafting.controller.ts @@ -0,0 +1,89 @@ +import { Response } from 'express'; +import { supabase } from '../lib/supabase'; +import { AuthenticatedRequest } from '../middleware/auth'; +import { callAiModelRaw } from './document.controller'; + +const DRAFT_SYSTEM_PROMPT = ` + Sen uzman bir Türk avukatısın. Sana verilen dosya bilgilerine göre, istenen türde + bir dilekçe TASLAĞI yazacaksın. Türk hukuk uygulamasına uygun, resmî bir dille yaz + (hitap, hususlar, netice-i talep gibi standart dilekçe yapısını kullan). + + EĞER sana bir "BÜRO ŞABLONU" verilmişse: bu şablon, bu hukuk bürosunun kendi imzası + olan format/dil/üslup tercihidir — YALNIZCA bunun yapısını, dilini, başlıklandırmasını + ve üslubunu takip et. Şablondaki taraf isimleri, esas no'su, tarihler, mahkeme adı gibi + ESKİ dosyaya ait somut bilgileri ASLA kullanma veya kopyalama — onların yerine SADECE + aşağıda verilen YENİ dosyanın gerçek bilgilerini koy. + + Sadece dilekçe taslağının kendisini döndür — başlık, açıklama veya ek yorum ekleme. + `; + +// Dilekçe Hazırlama ekranındaki "AI ile Taslak Oluştur" — dosyanın gerçek bilgilerini +// (başlık, UYAP'tan gelen taraflar, varsa Dosya Özeti) ve kullanıcının notlarını, +// isteğe bağlı olarak seçilen büro şablonunun DİLİNİ/FORMATINI referans alarak +// gerçek bir AI taslağı üretir. +export const generateDraft = async (req: AuthenticatedRequest, res: Response) => { + try { + const { case_id, petition_type, notes, template_id } = req.body || {}; + const userId = req.user?.id; + + if (!case_id || !petition_type) { + return res.status(400).json({ error: 'case_id ve petition_type zorunludur.' }); + } + + const { data: caseRow, error: caseError } = await supabase + .from('cases') + .select('id, title, parties, user_id') + .eq('id', case_id) + .single(); + if (caseError || !caseRow) { + return res.status(404).json({ error: 'Dava dosyası bulunamadı.' }); + } + if (caseRow.user_id !== userId) { + return res.status(403).json({ error: 'Bu dosyaya erişim yetkiniz yok.' }); + } + + const { data: lastAnalysis } = await supabase + .from('analyses') + .select('summary_json') + .eq('case_id', case_id) + .order('created_at', { ascending: false }) + .limit(1) + .maybeSingle(); + + let template: { name: string; extracted_text: string | null } | null = null; + if (template_id) { + const { data: templateRow } = await supabase + .from('templates') + .select('name, extracted_text, user_id') + .eq('id', template_id) + .single(); + if (templateRow && templateRow.user_id === userId) { + template = templateRow; + } + } + + const partiesText = Array.isArray(caseRow.parties) && caseRow.parties.length > 0 + ? caseRow.parties.map((p: any) => `${p.adi || ''} (${p.rol || ''}${p.vekil ? `, Vekil: ${p.vekil}` : ''})`).join('; ') + : 'Belirtilmemiş — UYAP Taraf Bilgileri sekmesi henüz çekilmemiş olabilir.'; + + const ozet = lastAnalysis?.summary_json?.ozet; + + let userContent = `YENİ DOSYA BİLGİLERİ:\nDosya: ${caseRow.title}\nTaraflar: ${partiesText}\nDilekçe Türü: ${petition_type}\n`; + if (notes && String(notes).trim()) { + userContent += `Vurgulanmasını istediğiniz noktalar: ${String(notes).trim()}\n`; + } + if (ozet && typeof ozet === 'string') { + userContent += `\nDosya Özeti (bağlam için):\n${ozet.slice(0, 4000)}\n`; + } + if (template?.extracted_text) { + userContent += `\n---\nBÜRO ŞABLONU "${template.name}" (SADECE dil/format/üslup referansı — içeriğini kopyalama):\n${template.extracted_text.slice(0, 6000)}\n`; + } + + const { aiContent, aiModel } = await callAiModelRaw(DRAFT_SYSTEM_PROMPT, userContent); + + res.json({ draft: aiContent, model: aiModel, usedTemplate: !!template }); + } catch (error: any) { + console.error('Generate Draft Error:', error); + res.status(500).json({ error: error.message || 'Internal Server Error' }); + } +}; diff --git a/src/controllers/template.controller.ts b/src/controllers/template.controller.ts index b74b1f0..0a07672 100644 --- a/src/controllers/template.controller.ts +++ b/src/controllers/template.controller.ts @@ -1,10 +1,15 @@ import { Response } from 'express'; import { supabase } from '../lib/supabase'; import { AuthenticatedRequest } from '../middleware/auth'; +import { extractTextFromUploadedFile } from './document.controller'; // Belge Şablonları: avukatın büro genelinde kullandığı dilekçe/evrak şablonları. // Ayrı bir Storage bucket açmaya gerek kalmasın diye documents'ın kullandığı // "case-documents" bucket'ı "templates/" öneki altında paylaşılıyor. +// +// Metni de çıkarıyoruz (documents'taki gibi) — Dilekçe Hazırlama'da bir şablon +// seçildiğinde AI'nin bu metni büronun kendi dili/formatı için referans alması +// gerekiyor (bkz. drafting.controller.ts). export const uploadTemplate = async (req: AuthenticatedRequest, res: Response) => { try { const file = req.file; @@ -15,6 +20,15 @@ export const uploadTemplate = async (req: AuthenticatedRequest, res: Response) = return res.status(400).json({ error: 'file ve name zorunludur.' }); } + let extractedText: string | null = null; + try { + extractedText = await extractTextFromUploadedFile(file); + } catch (err) { + // Metin çıkarımı başarısız olsa da şablon yine de yüklenip indirilebilir olsun — + // sadece AI stil referansı olarak kullanılamayacak. + console.error('Template Text Extraction Error:', err); + } + const storagePath = `templates/${userId}/${Date.now()}_${file.originalname}`; const { error: uploadError } = await supabase.storage .from('case-documents') @@ -34,6 +48,7 @@ export const uploadTemplate = async (req: AuthenticatedRequest, res: Response) = storage_path: storagePath, file_size: file.size, mime_type: file.mimetype, + extracted_text: extractedText, }]) .select() .single(); diff --git a/src/routes/drafting.routes.ts b/src/routes/drafting.routes.ts new file mode 100644 index 0000000..db74351 --- /dev/null +++ b/src/routes/drafting.routes.ts @@ -0,0 +1,9 @@ +import { Router } from 'express'; +import { requireAuth } from '../middleware/auth'; +import { generateDraft } from '../controllers/drafting.controller'; + +const router = Router(); + +router.post('/generate', requireAuth, generateDraft); + +export default router; diff --git a/src/routes/index.ts b/src/routes/index.ts index 52285ab..84bf042 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -4,6 +4,7 @@ import chatRoutes from './chat.routes'; import precedentRoutes from './precedent.routes'; import caseRoutes from './case.routes'; import templateRoutes from './template.routes'; +import draftingRoutes from './drafting.routes'; const router = Router(); @@ -15,6 +16,7 @@ router.use('/documents', documentRoutes); router.use('/chat', chatRoutes); router.use('/cases', caseRoutes); router.use('/templates', templateRoutes); +router.use('/drafting', draftingRoutes); router.use('/', precedentRoutes); // Keeps the same paths like /api/search-precedents export default router;