// AI Asistan — uçan buton + sohbet paneli.
// Dış bir AI servisine bağlanmaz: kural tabanlı, tamamen yerel bir niyet
// tanıma motoru kullanır. Ağ isteği yok, API anahtarı yok, rate limit yok —
// yanıtlar doğrudan uygulamanın kendi verisinden (state) üretilir.
const AWIcons = window.Icons;
const { isOverdue: awOverdue, uid: awUid } = window.PMStore;
const AW_HISTORY_KEY = 'navia_ai_chat_v1';
function awLoadHistory() {
try { return JSON.parse(localStorage.getItem(AW_HISTORY_KEY) || '[]'); } catch (e) { return []; }
}
function awSaveHistory(msgs) {
try { localStorage.setItem(AW_HISTORY_KEY, JSON.stringify(msgs.slice(-30))); } catch (e) {}
}
function awNorm(s) { return (s || '').toLocaleLowerCase('tr-TR').trim(); }
function awDateKey(d) { return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; }
// JS'in \b sınırı yalnızca ASCII [A-Za-z0-9_] tanır — ç/ğ/ı/ö/ş/ü ile başlayan/biten
// kelimelerde (örn. "kaç", "önemli") sessizce başarısız olur. Türkçe-güvenli alternatif.
const AW_LETTERS = 'a-zçğıiöşü';
function awWord(alt) {
return new RegExp(`(^|[^${AW_LETTERS}])(${alt})($|[^${AW_LETTERS}])`);
}
function awParseDue(text) {
// Türkçe hal ekleri ("yarına", "bugüne") gövdeye eklendiği için sondaki \b aranmaz —
// yalnızca kelime başlangıcı kontrol edilir.
const now = new Date(); now.setHours(0, 0, 0, 0);
if (awWord('öbür\\s*gün').test(text)) { const d = new Date(now); d.setDate(d.getDate() + 2); return awDateKey(d); }
if (/\bbugün/.test(text)) return awDateKey(now);
if (/\byarın/.test(text)) { const d = new Date(now); d.setDate(d.getDate() + 1); return awDateKey(d); }
const m = text.match(/(\d+)\s*gün\s*sonra/);
if (m) { const d = new Date(now); d.setDate(d.getDate() + parseInt(m[1], 10)); return awDateKey(d); }
if (/\bgelecek\s*hafta\b|\bhaftaya\b/.test(text)) { const d = new Date(now); d.setDate(d.getDate() + 7); return awDateKey(d); }
return null;
}
// Tırnak içi > "diye" kalıbı > "X görev ekle" kalıbı sırasıyla başlık çıkarmayı dener.
function awExtractTitle(raw) {
const q = raw.match(/["'“”‘’]([^"'“”‘’]{2,90})["'“”‘’]/);
if (q) return q[1].trim();
const d = raw.match(/^(.*?)\s+diye\b/i);
if (d) {
let t = d[1].trim().replace(/^(yarına|bugüne|haftaya|gelecek hafta|öbür güne)\s+/i, '').trim();
if (t) return t;
}
const g = raw.match(/^(.*?)\s+(için\s+)?(görev|iş)\s*(olarak)?\s*(ekle|oluştur)/i);
if (g && g[1].trim()) return g[1].trim();
return null;
}
// Metin içinde geçen en uzun (en spesifik) görev başlığını bulur.
function awFindMentionedTask(tasks, textLower) {
let best = null, bestLen = 0;
for (const t of tasks) {
const tl = awNorm(t.title);
if (tl.length >= 3 && textLower.includes(tl) && tl.length > bestLen) { best = t; bestLen = tl.length; }
}
return best;
}
// **kalın** işaretli metni satır satır React node'larına çevirir — chat balonlarında
// düz yıldız karakteri yerine gerçek kalın yazı görünsün diye.
function awRenderContent(text) {
const lines = (text || '').split('\n');
return lines.map((line, li) => {
const parts = line.split(/(\*\*[^*]+\*\*)/g).filter(Boolean);
return (
{li > 0 && }
{parts.map((part, pi) => (
part.startsWith('**') && part.endsWith('**')
? {part.slice(2, -2)}
: {part}
))}
);
});
}
const AW_HELP = [
'Şunları deneyebilirsin:',
'· Bugün ne yapmam gerekiyor?',
'· Gecikmiş görevlerim neler?',
'· Kaç açık görevim var?',
'· **"Toplantı notu hazırla"** diye yarına görev ekle',
'· Toplantı notu hazırla görevini tamamla',
'· Toplantı notu hazırla görevini sil',
].join('\n');
// Kural tabanlı yerel yanıt motoru — { reply, action } döner. Ağ isteği yapmaz.
function awLocalReply(rawText, state) {
const text = awNorm(rawText);
const today = new Date(); today.setHours(0, 0, 0, 0);
const todayKey = awDateKey(today);
const tasks = (state.tasks || []).filter(t => !t.archived);
const openTasks = tasks.filter(t => t.status !== 'done');
if (/^(merhaba|selam|hey|naber|nasılsın|iyi günler|günaydın)\b/.test(text)) {
return { reply: 'Merhaba! Görevlerin hakkında sorular sorabilir ya da benden görev ekleyip tamamlamamı isteyebilirsin.\n\n' + AW_HELP };
}
if (/\b(yardım|ne yapabilirsin|neler yapabilirsin)\b/.test(text)) {
return { reply: AW_HELP };
}
// Görev oluşturma — "sil/tamamla" ile çakışmasın diye önce elenir
if (/\b(ekle|oluştur)\b/.test(text) && /\b(görev|iş)/.test(text) && !/\bsil\b/.test(text)) {
const title = awExtractTitle(rawText);
if (!title) {
return { reply: 'Görev başlığını tam olarak anlayamadım. Şu kalıbı dener misin:\n"Görev adı" diye yarına görev ekle' };
}
const due = awParseDue(text);
const priority = (/\byüksek\b|\bacil\b/.test(text) || awWord('önemli').test(text)) ? 'high' : /\bdüşük\b/.test(text) ? 'low' : 'medium';
return {
reply: `Tamam, ${due ? '**' + due + '** tarihine ' : ''}**${title}** görevini ekliyorum.`,
action: { type: 'createTask', title, due, priority },
};
}
// Sprint oluşturma
if (/\bsprint\b/.test(text) && /\b(ekle|oluştur)\b/.test(text)) {
const name = awExtractTitle(rawText) || 'Yeni Sprint';
return { reply: `Tamam, **${name}** adında bir sprint oluşturuyorum.`, action: { type: 'createSprint', name } };
}
// Görev silme
if (/\bsil\b/.test(text)) {
const task = awFindMentionedTask(openTasks.length ? openTasks : tasks, text);
if (!task) return { reply: 'Hangi görevi silmemi istersin? Tam başlığını yazar mısın?' };
return { reply: `**${task.title}** görevini siliyorum.`, action: { type: 'deleteTask', taskTitle: task.title } };
}
// Görev tamamlama
if (/\btamamla\b|\bbitir\b|\bbitti\b/.test(text)) {
const task = awFindMentionedTask(openTasks, text);
if (!task) return { reply: 'Hangi görevi tamamlandı olarak işaretleyeyim? Tam başlığını yazar mısın?' };
return { reply: `**${task.title}** görevini tamamlandı olarak işaretliyorum.`, action: { type: 'updateTask', taskTitle: task.title, patch: { status: 'done' } } };
}
// Görev başlatma
if (/\bbaşlat\b/.test(text)) {
const task = awFindMentionedTask(tasks.filter(t => t.status === 'todo'), text);
if (!task) return { reply: 'Hangi görevi devam ediyor durumuna almamı istersin?' };
return { reply: `**${task.title}** görevini devam ediyor durumuna alıyorum.`, action: { type: 'updateTask', taskTitle: task.title, patch: { status: 'doing' } } };
}
// Gecikmiş görevler
if (/gecik/.test(text)) {
const overdue = openTasks.filter(t => awOverdue?.(t.due, t.status));
if (!overdue.length) return { reply: 'Gecikmiş görevin yok — her şey yolunda 🎉' };
return { reply: `**${overdue.length}** gecikmiş görevin var:\n` + overdue.slice(0, 10).map(t => `· **${t.title}** (${t.due})`).join('\n') };
}
// Bugün teslim görevler
if (/\bbugün/.test(text) && /(görev|iş|yapmam|teslim)/.test(text)) {
const dueToday = openTasks.filter(t => t.due === todayKey);
if (!dueToday.length) return { reply: 'Bugün teslim edilmesi gereken bir görevin yok.' };
return { reply: `Bugün teslim olan **${dueToday.length}** görev var:\n` + dueToday.map(t => `· **${t.title}**`).join('\n') };
}
// Sayım sorguları
if ((awWord('kaç').test(text) || /\btoplam\b/.test(text)) && /(görev|iş)/.test(text)) {
return { reply: `**${openTasks.length}** açık, **${tasks.length - openTasks.length}** tamamlanmış görevin var. Toplam **${tasks.length}** görev.` };
}
// Tamamlanan görevler
if (/tamamlanan|biten görev/.test(text)) {
const doneThisMonth = tasks.filter(t => t.status === 'done' && (t.completedAt || '').slice(0, 7) === todayKey.slice(0, 7));
return { reply: `Bu ay **${doneThisMonth.length}** görev tamamladın. Toplamda **${tasks.filter(t => t.status === 'done').length}** görev tamamlanmış.` };
}
// Proje listesi
if (/proje/.test(text) && /(liste|neler|hangi|kaç)/.test(text)) {
const projects = state.projects || [];
if (!projects.length) return { reply: 'Henüz hiç projen yok.' };
return { reply: `**${projects.length}** projen var:\n` + projects.map(p => `· ${p.name}`).join('\n') };
}
return { reply: 'Bunu tam anlayamadım. ' + AW_HELP };
}
function AIWidget({ state, session, onCreateTask, onUpdateTask, onDeleteTask, onCreateSprint }) {
const [open, setOpen] = React.useState(false);
const [messages, setMessages] = React.useState(awLoadHistory);
const [input, setInput] = React.useState('');
const [loading, setLoading] = React.useState(false);
const listRef = React.useRef(null);
const isPro = state.plan === 'pro' || state.isAdmin;
React.useEffect(() => { awSaveHistory(messages); }, [messages]);
React.useEffect(() => {
if (listRef.current) listRef.current.scrollTop = listRef.current.scrollHeight;
}, [messages, open, loading]);
const applyAction = (action) => {
if (!action || !action.type) return null;
try {
if (action.type === 'createTask') {
onCreateTask({
id: awUid(),
title: action.title || 'Yeni görev',
due: action.due || null,
priority: ['low', 'medium', 'high'].includes(action.priority) ? action.priority : 'medium',
projectId: state.activeProjectId || (state.projects[0] && state.projects[0].id) || null,
});
return `✓ Görev oluşturuldu: **${action.title || 'Yeni görev'}**`;
}
if (action.type === 'updateTask') {
const task = (state.tasks || []).find(t => t.title === action.taskTitle);
if (!task) return null;
onUpdateTask(task.id, action.patch || {});
return `✓ **${task.title}** güncellendi.`;
}
if (action.type === 'deleteTask') {
const task = (state.tasks || []).find(t => t.title === action.taskTitle);
if (!task) return null;
onDeleteTask(task.id);
return `✓ **${task.title}** silindi.`;
}
if (action.type === 'createSprint') {
onCreateSprint({
projectId: state.activeProjectId || (state.projects[0] && state.projects[0].id) || null,
name: action.name || 'Yeni Sprint',
goal: '',
status: 'planning',
});
return `✓ Sprint oluşturuldu: **${action.name || 'Yeni Sprint'}**`;
}
} catch (e) {
return '⚠ Eylem uygulanırken bir sorun oluştu.';
}
return null;
};
const send = () => {
const text = input.trim();
if (!text || loading) return;
setInput('');
const nextMsgs = [...messages, { role: 'user', content: text }];
setMessages(nextMsgs);
setLoading(true);
// Küçük bir gecikme — anlık yanıt "düşünmeden atıyor" hissi vermesin
setTimeout(() => {
const { reply, action } = awLocalReply(text, state);
const actionNote = applyAction(action);
const finalMsgs = [...nextMsgs, { role: 'assistant', content: reply }];
if (actionNote) finalMsgs.push({ role: 'system', content: actionNote });
setMessages(finalMsgs);
setLoading(false);
}, 280);
};
const clearChat = () => { setMessages([]); awSaveHistory([]); };
return (
<>
setOpen(v => !v)}
title="AI Asistan"
aria-label="AI Asistan"
>
{open ? : }
{open && (
AI Asistan
{isPro ? 'Görev ve proje konusunda sor' : 'Pro plana özel'}
{isPro && messages.length > 0 && (
)}
{!isPro ? (
🚀
AI Asistan, görevlerini konuşarak yönetmene yardımcı olur — Pro plana özel bir özellik.
Pro'ya Geç — Bilgi Al
) : (
<>
{messages.length === 0 && (
Merhaba{session?.name ? ', ' + session.name.split(' ')[0] : ''} 👋
"Bugün ne yapmam gerekiyor?", "Gecikmiş işlerim neler?" gibi sorular sorabilir,
ya da "'X' diye yarına görev ekle" diyebilirsin.
)}
{messages.map((m, i) => (
{awRenderContent(m.content)}
))}
{loading &&
}
>
)}
)}
>
);
}
window.AIWidget = AIWidget;