POST /api/cases lets the frontend actually create a new dava dosyası (no working "Yeni Dosya" flow existed before). chatMessage now generates a short title from a case's first message (like Claude/ ChatGPT auto-titling) and renames the case, returned as `newTitle`.
200 lines
7.8 KiB
TypeScript
200 lines
7.8 KiB
TypeScript
import { Response } from 'express';
|
||
import { supabase } from '../lib/supabase';
|
||
import { AuthenticatedRequest } from '../middleware/auth';
|
||
|
||
// Bir dosyanın ilk mesajından kısa bir başlık üretir (Claude/ChatGPT'nin yaptığı gibi).
|
||
// Başarısız olursa null döner — çağıran taraf bu durumda "Yeni Sohbet" başlığını korur,
|
||
// asıl sohbet cevabını etkilemez.
|
||
async function generateCaseTitle(firstMessage: string): Promise<string | null> {
|
||
try {
|
||
const aiModel = process.env.AI_MODEL || 'mizan-fixed';
|
||
const runpodUrl = process.env.RUNPOD_API_BASE_URL || 'https://q190env94stwis-11434.proxy.runpod.net';
|
||
const response = await fetch(`${runpodUrl}/api/chat`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
model: aiModel,
|
||
messages: [
|
||
{
|
||
role: 'system',
|
||
content: 'Kullanıcının hukuki sorusunu en fazla 5 kelimelik, kısa ve açıklayıcı bir Türkçe başlığa çevir. Sadece başlığı yaz; tırnak işareti, noktalama veya başka açıklama ekleme.'
|
||
},
|
||
{ role: 'user', content: firstMessage.slice(0, 2000) },
|
||
],
|
||
think: false,
|
||
stream: false,
|
||
}),
|
||
});
|
||
if (!response.ok) return null;
|
||
const data = await response.json();
|
||
const raw = (data.message?.content || '').toString().trim().replace(/^["'“”]+|["'“”]+$/g, '');
|
||
if (!raw) return null;
|
||
return raw.slice(0, 80);
|
||
} catch (err) {
|
||
console.error('Generate Case Title Error:', err);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
export const deleteChatHistory = async (req: AuthenticatedRequest, res: Response) => {
|
||
try {
|
||
const { caseId } = req.params;
|
||
if (!caseId || caseId === 'general') {
|
||
return res.status(400).json({ error: 'Geçersiz caseId' });
|
||
}
|
||
// requireAuth zaten bu caseId'nin isteği yapan kullanıcıya ait olduğunu doğruladı.
|
||
// Silme işlemi burada (backend'de, service_role ile) yapılıyor — frontend'in anon
|
||
// client'ı chat_messages'a RLS nedeniyle yazamıyor/silemiyor olabilir.
|
||
const { error } = await supabase.from('chat_messages').delete().eq('case_id', caseId);
|
||
if (error) throw error;
|
||
res.json({ ok: true });
|
||
} catch (error: any) {
|
||
console.error('Delete Chat History Error:', error);
|
||
res.status(500).json({ error: error.message || 'Internal Server Error' });
|
||
}
|
||
};
|
||
|
||
export const chatMessage = async (req: AuthenticatedRequest, res: Response) => {
|
||
try {
|
||
const { caseId } = req.params;
|
||
const { content } = req.body;
|
||
const userId = req.user?.id;
|
||
|
||
if (!content || !caseId) {
|
||
return res.status(400).json({ error: 'Missing caseId or content' });
|
||
}
|
||
|
||
// "general" = dosyasız/genel sohbet. Şemada chat_messages.case_id NOT NULL bir
|
||
// cases FK'si olduğundan bu moddaki mesajları veritabanına yazmıyoruz — sadece
|
||
// AI'ye iletip yanıtı doğrudan döndürüyoruz.
|
||
if (caseId === 'general') {
|
||
const generalSystemPrompt = 'Sen bir hukuk asistanısın. Belirli bir dava dosyasına bağlı olmayan genel hukuki soruları cevaplıyorsun. Emin olmadığın noktalarda bunu açıkça belirt, uydurma bilgi verme.';
|
||
const aiModel = process.env.AI_MODEL || 'mizan-fixed';
|
||
const runpodUrl = process.env.RUNPOD_API_BASE_URL || 'https://q190env94stwis-11434.proxy.runpod.net';
|
||
|
||
const response = await fetch(`${runpodUrl}/api/chat`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
model: aiModel,
|
||
messages: [
|
||
{ role: 'system', content: generalSystemPrompt },
|
||
{ role: 'user', content },
|
||
],
|
||
think: false,
|
||
stream: false,
|
||
}),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`AI Model request failed: ${response.statusText}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
const aiContent = data.message?.content || 'Yanıt alınamadı.';
|
||
|
||
return res.json({
|
||
message: { id: `general-${Date.now()}`, case_id: 'general', role: 'assistant', content: aiContent },
|
||
});
|
||
}
|
||
|
||
const { data: userMessage, error: userMsgError } = await supabase
|
||
.from('chat_messages')
|
||
.insert([{ case_id: caseId, user_id: userId, role: 'user', content }])
|
||
.select().single();
|
||
if (userMsgError) throw userMsgError;
|
||
|
||
// 1. Dosyaya ait belge analizlerini çek (EKSİK OLAN KISIM)
|
||
const { data: analyses } = await supabase
|
||
.from('analyses')
|
||
.select('summary_json')
|
||
.eq('case_id', caseId);
|
||
|
||
const { data: documents } = await supabase
|
||
.from('documents')
|
||
.select('filename, extracted_text')
|
||
.eq('case_id', caseId);
|
||
|
||
// 2. Geçmiş sohbet
|
||
const { data: pastMessages } = await supabase
|
||
.from('chat_messages')
|
||
.select('role, content')
|
||
.eq('case_id', caseId)
|
||
.order('created_at', { ascending: true })
|
||
.limit(10);
|
||
|
||
// 3. Belge bağlamını sistem promptuna göm
|
||
let documentContext = '';
|
||
if (documents && documents.length > 0) {
|
||
documentContext = '\n\nDOSYAYA AİT BELGELER:\n' +
|
||
documents.map(d => `--- ${d.filename} ---\n${d.extracted_text}`).join('\n\n');
|
||
} else {
|
||
documentContext = '\n\nBu dosyaya henüz hiçbir belge yüklenmemiştir. Genel hukuki bilgiyle cevap ver, ama bunu kullanıcıya açıkça belirt.';
|
||
}
|
||
|
||
if (analyses && analyses.length > 0) {
|
||
documentContext += '\n\nÖNCEKİ ANALİZ ÖZETİ:\n' + JSON.stringify(analyses[analyses.length - 1].summary_json);
|
||
}
|
||
|
||
const systemPrompt = `Sen bir hukuk asistanısın. Bu dava dosyasıyla ilgili kullanıcının sorularını, aşağıda verilen belge içeriğine dayanarak cevaplıyorsun. Eğer belge yoksa veya soruyu cevaplamaya yetmiyorsa, bunu açıkça belirt, uydurma bilgi verme.${documentContext}`;
|
||
|
||
const messages: any[] = [{ role: 'system', content: systemPrompt }];
|
||
if (pastMessages) {
|
||
pastMessages.forEach(m => messages.push({ role: m.role, content: m.content }));
|
||
} else {
|
||
messages.push({ role: 'user', content });
|
||
}
|
||
|
||
const aiModel = process.env.AI_MODEL || 'mizan-fixed';
|
||
// Native endpoint'e geçiş
|
||
const runpodUrl = process.env.RUNPOD_API_BASE_URL || 'https://q190env94stwis-11434.proxy.runpod.net';
|
||
|
||
const response = await fetch(`${runpodUrl}/api/chat`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
model: aiModel,
|
||
messages: messages,
|
||
think: false,
|
||
stream: false,
|
||
}),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`AI Model request failed: ${response.statusText}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
const aiContent = data.message?.content || 'Yanıt alınamadı.';
|
||
|
||
const { data: aiMessage, error: aiMsgError } = await supabase
|
||
.from('chat_messages')
|
||
.insert([{ case_id: caseId, user_id: userId, role: 'assistant', content: aiContent }])
|
||
.select().single();
|
||
if (aiMsgError) throw aiMsgError;
|
||
|
||
// Bu dosyanın ilk mesajıysa (kullanıcının bu isteklen önce hiç mesajı yoktu),
|
||
// ilk mesajdan kısa bir başlık üretip dosyayı yeniden adlandır.
|
||
let newTitle: string | null = null;
|
||
const isFirstMessage = !pastMessages || pastMessages.length <= 1;
|
||
if (isFirstMessage) {
|
||
newTitle = await generateCaseTitle(content);
|
||
if (newTitle) {
|
||
const { error: titleError } = await supabase
|
||
.from('cases')
|
||
.update({ title: newTitle })
|
||
.eq('id', caseId);
|
||
if (titleError) {
|
||
console.error('Case Title Update Error:', titleError);
|
||
newTitle = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
res.json({ message: aiMessage, newTitle });
|
||
} catch (error: any) {
|
||
console.error('Chat Error:', error);
|
||
res.status(500).json({ error: error.message || 'Internal Server Error' });
|
||
}
|
||
};
|