feat: persist general chat as chat_threads, decoupled from cases
Sohbet/Asistan sayfasındaki genel sohbetler artık kalıcı: yeni chat_threads/thread_messages tabloları (cases ile ilişkisiz) ile sendThreadMessage endpoint'i eklendi. Eski caseId==='general' (kalıcı olmayan) dalı kaldırıldı, AI çağrısı ortak callAiChat yardımcısına taşındı. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
982f908bf9
commit
0318f2ac77
@@ -3,10 +3,11 @@ import { supabase } from '../lib/supabase';
|
||||
import { AuthenticatedRequest } from '../middleware/auth';
|
||||
import { getAiConfig } from '../lib/aiConfig';
|
||||
|
||||
// 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> {
|
||||
// İlk mesajdan 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 varsayılan başlığı korur, asıl
|
||||
// sohbet cevabını etkilemez. Hem dava-dosyası-siz "Yeni Sohbet" (cases.kind='chat')
|
||||
// hem de chat_threads kayıtları için ortak kullanılır.
|
||||
async function generateShortTitle(firstMessage: string): Promise<string | null> {
|
||||
try {
|
||||
const { runpodUrl, aiModel } = getAiConfig();
|
||||
const response = await fetch(`${runpodUrl}/api/chat`, {
|
||||
@@ -31,11 +32,29 @@ async function generateCaseTitle(firstMessage: string): Promise<string | null> {
|
||||
if (!raw) return null;
|
||||
return raw.slice(0, 80);
|
||||
} catch (err) {
|
||||
console.error('Generate Case Title Error:', err);
|
||||
console.error('Generate Short Title Error:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// AI modeline chat completion isteği atan ortak yardımcı. RunPod worker uykudaysa
|
||||
// soğuk başlangıç birkaç dakika sürebilir; burada sunucu tarafında bir zaman aşımı
|
||||
// dayatmıyoruz (istemci kendi AbortController'ıyla bekleme süresini yönetiyor) —
|
||||
// böylece yavaş ama başarılı olacak bir yanıtı erkenden kesmiyoruz.
|
||||
async function callAiChat(messages: { role: string; content: string }[]): Promise<string> {
|
||||
const { runpodUrl, aiModel } = getAiConfig();
|
||||
const response = await fetch(`${runpodUrl}/api/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ model: aiModel, messages, think: false, stream: false }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`AI Model request failed: ${response.statusText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
return data.message?.content || 'Yanıt alınamadı.';
|
||||
}
|
||||
|
||||
export const deleteChatHistory = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { caseId } = req.params;
|
||||
@@ -64,39 +83,6 @@ export const chatMessage = async (req: AuthenticatedRequest, res: Response) => {
|
||||
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 { runpodUrl, aiModel } = getAiConfig();
|
||||
|
||||
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 },
|
||||
});
|
||||
}
|
||||
|
||||
// kind='case' olan gerçek dava dosyalarının başlığı UYAP'tan/kullanıcıdan geliyor —
|
||||
// bu asla otomatik yeniden adlandırılmamalı. Otomatik başlık üretimi SADECE
|
||||
// kind='chat' (Sohbet'teki "Yeni Sohbet") kayıtları için geçerli.
|
||||
@@ -150,25 +136,7 @@ export const chatMessage = async (req: AuthenticatedRequest, res: Response) => {
|
||||
messages.push({ role: 'user', content });
|
||||
}
|
||||
|
||||
const { runpodUrl, aiModel } = getAiConfig();
|
||||
|
||||
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 aiContent = await callAiChat(messages);
|
||||
|
||||
const { data: aiMessage, error: aiMsgError } = await supabase
|
||||
.from('chat_messages')
|
||||
@@ -182,7 +150,7 @@ export const chatMessage = async (req: AuthenticatedRequest, res: Response) => {
|
||||
let newTitle: string | null = null;
|
||||
const isFirstMessage = !pastMessages || pastMessages.length <= 1;
|
||||
if (isFirstMessage && caseKind === 'chat') {
|
||||
newTitle = await generateCaseTitle(content);
|
||||
newTitle = await generateShortTitle(content);
|
||||
if (newTitle) {
|
||||
const { error: titleError } = await supabase
|
||||
.from('cases')
|
||||
@@ -201,3 +169,77 @@ export const chatMessage = async (req: AuthenticatedRequest, res: Response) => {
|
||||
res.status(500).json({ error: error.message || 'Internal Server Error' });
|
||||
}
|
||||
};
|
||||
|
||||
// Genel (dosyasız) Sohbet/Asistan sayfası için — chat_threads/thread_messages
|
||||
// tabloları cases ile hiçbir ilişkiye sahip değil (bkz. sql/9_chat_threads.sql).
|
||||
// Thread oluşturma/silme/geçmiş okuma doğrudan frontend'den Supabase ile yapılıyor
|
||||
// (events/clients/drafts ile aynı desen); burada sadece AI çağrısı + mesaj kaydı var.
|
||||
export const sendThreadMessage = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { threadId } = req.params;
|
||||
const { content } = req.body;
|
||||
const userId = req.user?.id;
|
||||
|
||||
if (!content || !threadId) {
|
||||
return res.status(400).json({ error: 'Missing threadId or content' });
|
||||
}
|
||||
|
||||
const { data: thread } = await supabase
|
||||
.from('chat_threads')
|
||||
.select('id, user_id')
|
||||
.eq('id', threadId)
|
||||
.maybeSingle();
|
||||
if (!thread || thread.user_id !== userId) {
|
||||
return res.status(403).json({ error: 'Bu sohbete erişim yetkiniz yok.' });
|
||||
}
|
||||
|
||||
const { error: userMsgError } = await supabase
|
||||
.from('thread_messages')
|
||||
.insert([{ thread_id: threadId, user_id: userId, role: 'user', content }]);
|
||||
if (userMsgError) throw userMsgError;
|
||||
|
||||
const { data: pastMessages } = await supabase
|
||||
.from('thread_messages')
|
||||
.select('role, content')
|
||||
.eq('thread_id', threadId)
|
||||
.order('created_at', { ascending: true })
|
||||
.limit(20);
|
||||
|
||||
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 messages: any[] = [{ role: 'system', content: generalSystemPrompt }];
|
||||
if (pastMessages && pastMessages.length > 0) {
|
||||
pastMessages.forEach(m => messages.push({ role: m.role, content: m.content }));
|
||||
} else {
|
||||
messages.push({ role: 'user', content });
|
||||
}
|
||||
|
||||
const aiContent = await callAiChat(messages);
|
||||
|
||||
const { data: aiMessage, error: aiMsgError } = await supabase
|
||||
.from('thread_messages')
|
||||
.insert([{ thread_id: threadId, user_id: userId, role: 'assistant', content: aiContent }])
|
||||
.select().single();
|
||||
if (aiMsgError) throw aiMsgError;
|
||||
|
||||
let newTitle: string | null = null;
|
||||
const isFirstMessage = !pastMessages || pastMessages.length <= 1;
|
||||
if (isFirstMessage) {
|
||||
newTitle = await generateShortTitle(content);
|
||||
if (newTitle) {
|
||||
const { error: titleError } = await supabase
|
||||
.from('chat_threads')
|
||||
.update({ title: newTitle })
|
||||
.eq('id', threadId);
|
||||
if (titleError) {
|
||||
console.error('Thread Title Update Error:', titleError);
|
||||
newTitle = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ message: aiMessage, newTitle });
|
||||
} catch (error: any) {
|
||||
console.error('Send Thread Message Error:', error);
|
||||
res.status(500).json({ error: error.message || 'Internal Server Error' });
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user