Refactor code structure for improved readability and maintainability

This commit is contained in:
Poyraz Avsever
2026-05-08 11:18:55 +03:00
parent e98972597d
commit 9adc34418e
21 changed files with 1194 additions and 393 deletions
+38 -38
View File
@@ -1,19 +1,19 @@
"use client";
import { useState } from "react";
import { v4 as uuidv4 } from "uuid";
import { useLiveQuery } from "dexie-react-hooks";
import { db } from "@/lib/db";
import { v4 as uuidv4 } from "uuid";
import { analyzeJournalWithLocalAI } from "@/lib/ai";
import { db } from "@/lib/db";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
CardDescription,
CardFooter,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
@@ -31,22 +31,23 @@ export default function JournalPage() {
const [energy, setEnergy] = useState("3");
const [isSubmitting, setIsSubmitting] = useState(false);
// Dexie.js üzerinden günlükleri tarih sırasına göre çekiyoruz (en yeni en üstte)
// Dexie.js üzerinden günlükleri tarih sırasına göre çekiyoruz (en yeni en üstte).
const journals = useLiveQuery(() =>
db.journals.orderBy("date").reverse().toArray(),
);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
if (!content.trim()) return;
setIsSubmitting(true);
const now = new Date().toISOString();
const entryId = uuidv4();
const currentContent = content; // Analiz için metni kopyala
const currentContent = content;
try {
// 1. Veriyi veritabanına hemen ekle (Kullanıcı beklemesin)
// Veriyi önce kaydet, AI analizini daha sonra arka planda tamamla.
await db.journals.add({
id: entryId,
date: now.split("T")[0],
@@ -57,12 +58,10 @@ export default function JournalPage() {
updated_at: now,
});
// UI'ı Sıfırla
setContent("");
setEnergy("3");
setMood("happy");
// 2. Arka planda AI Analizi başlat
try {
const aiResult = await analyzeJournalWithLocalAI(currentContent);
if (aiResult) {
@@ -72,8 +71,7 @@ export default function JournalPage() {
ai_summary: aiResult.ai_summary,
});
// Eğer AI görev önerdiyse Görevler tablosuna at (pending / onay bekliyor yapısı eklenebilir ama direkt atalım)
if (aiResult.suggested_tasks && aiResult.suggested_tasks.length > 0) {
if (aiResult.suggested_tasks?.length) {
for (const taskTitle of aiResult.suggested_tasks) {
await db.tasks.add({
id: uuidv4(),
@@ -87,8 +85,8 @@ export default function JournalPage() {
}
}
}
} catch (aiErr) {
console.error("Arka plan AI analizi hatası:", aiErr);
} catch (aiError) {
console.error("Arka plan AI analizi hatası:", aiError);
}
} catch (error) {
console.error("Günlük kaydedilemedi:", error);
@@ -104,7 +102,7 @@ export default function JournalPage() {
};
return (
<div className="space-y-6 max-w-4xl mx-auto animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="mx-auto max-w-4xl animate-in space-y-6 fade-in slide-in-from-bottom-4 duration-500">
<div>
<h1 className="text-3xl font-bold tracking-tight">Günlük</h1>
<p className="text-muted-foreground">
@@ -119,27 +117,28 @@ export default function JournalPage() {
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label>Ruh Hali</Label>
<div className="flex gap-2">
{moods.map((m) => (
{moods.map((currentMood) => (
<button
key={m.id}
key={currentMood.id}
type="button"
onClick={() => setMood(m.id)}
className={`flex-1 py-2 px-1 rounded-md border text-xl flex items-center justify-center transition-colors ${
mood === m.id
? "bg-primary/20 border-primary"
: "bg-card border-border hover:bg-muted"
onClick={() => setMood(currentMood.id)}
className={`flex flex-1 items-center justify-center rounded-md border px-1 py-2 text-xl transition-colors ${
mood === currentMood.id
? "border-primary bg-primary/20"
: "border-border bg-card hover:bg-muted"
}`}
title={m.label}
title={currentMood.label}
>
{m.emoji}
{currentMood.emoji}
</button>
))}
</div>
</div>
<div className="space-y-2">
<Label>Enerji Seviyesi (1-5): {energy}</Label>
<Input
@@ -148,7 +147,7 @@ export default function JournalPage() {
max="5"
step="1"
value={energy}
onChange={(e) => setEnergy(e.target.value)}
onChange={(event) => setEnergy(event.target.value)}
className="w-full"
/>
</div>
@@ -159,7 +158,7 @@ export default function JournalPage() {
<Textarea
id="content"
value={content}
onChange={(e) => setContent(e.target.value)}
onChange={(event) => setContent(event.target.value)}
className="min-h-[150px] resize-y"
placeholder="Örneğin: Bugün toplantıda işler ters gitti..."
/>
@@ -190,13 +189,14 @@ export default function JournalPage() {
<Card key={journal.id}>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-base flex items-center gap-2">
<CardTitle className="flex items-center gap-2 text-base">
<span>
{moods.find((m) => m.id === journal.mood)?.emoji}
{moods.find((currentMood) => currentMood.id === journal.mood)
?.emoji ?? "📝"}
</span>
<span>{journal.date}</span>
</CardTitle>
<span className="text-xs px-2 py-1 bg-muted rounded-md font-medium text-muted-foreground">
<span className="rounded-md bg-muted px-2 py-1 text-xs font-medium text-muted-foreground">
Enerji: {journal.energy}/5
</span>
</div>
@@ -206,30 +206,30 @@ export default function JournalPage() {
{journal.content}
</p>
{/* AI Sonuçlarını Göster */}
{journal.ai_tags && (
<div className="mt-4 flex flex-wrap gap-2">
{journal.ai_tags.map((tag) => (
<span
key={tag}
className="text-xs px-2 py-1 bg-primary/10 text-primary rounded-md"
className="rounded-md bg-primary/10 px-2 py-1 text-xs text-primary"
>
{tag}
</span>
))}
</div>
)}
{journal.ai_summary && (
<div className="mt-3 p-3 bg-muted/40 rounded-lg border text-sm text-muted-foreground italic border-l-2 border-l-primary">
" {journal.ai_summary} "
<div className="mt-3 rounded-lg border border-l-2 border-l-primary bg-muted/40 p-3 text-sm italic text-muted-foreground">
&ldquo;{journal.ai_summary}&rdquo;
</div>
)}
<div className="flex justify-end mt-4">
<div className="mt-4 flex justify-end">
<Button
variant="ghost"
size="sm"
className="text-red-500 hover:text-red-600 hover:bg-red-500/10"
className="text-red-500 hover:bg-red-500/10 hover:text-red-600"
onClick={() => handleDelete(journal.id)}
>
Sil
+43 -12
View File
@@ -1,20 +1,51 @@
import { Sidebar } from "@/components/layout/sidebar";
import { Header } from "@/components/layout/header";
import { DashboardShell } from "@/components/layout/dashboard-shell";
import { createClient } from "@/lib/supabase/server";
export default function DashboardLayout({
export default async function DashboardLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
const { data: profile } = user
? await supabase
.from("profiles")
.select("first_name, last_name, avatar_url")
.eq("id", user.id)
.maybeSingle()
: { data: null };
const fallbackName = user?.email?.split("@")[0] ?? "MindSpace Kullanıcısı";
const displayName =
[profile?.first_name, profile?.last_name].filter(Boolean).join(" ") ||
fallbackName;
const shortName = displayName
.split(" ")
.filter(Boolean)
.slice(0, 2)
.map((part) => part[0]?.toUpperCase())
.join("")
.slice(0, 2) || "MS";
return (
<div className="flex min-h-screen bg-background text-foreground">
<Sidebar />
<div className="flex flex-col flex-1 h-screen overflow-hidden">
<Header />
<main className="flex-1 overflow-y-auto p-6 md:p-8">
{children}
</main>
</div>
</div>
<DashboardShell
user={{
email: user?.email ?? "bilinmiyor@mindspace.local",
displayName,
shortName,
avatarUrl:
profile?.avatar_url ||
user?.user_metadata?.avatar_url ||
user?.user_metadata?.picture ||
null,
}}
>
{children}
</DashboardShell>
);
}
+81 -92
View File
@@ -1,23 +1,27 @@
"use client";
import { useMemo } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Activity, Brain, PenTool, CheckCircle } from "lucide-react";
import { useLiveQuery } from "dexie-react-hooks";
import { db } from "@/lib/db";
import { Activity, Brain, CheckCircle, PenTool } from "lucide-react";
import {
LineChart,
Bar,
BarChart,
CartesianGrid,
Legend,
Line,
LineChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
BarChart,
Bar,
Legend,
} from "recharts";
import { db, type Journal, type Task } from "@/lib/db";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
const EMPTY_JOURNALS: Journal[] = [];
const EMPTY_TASKS: Task[] = [];
const moodScores: Record<string, number> = {
happy: 4,
neutral: 3,
@@ -26,65 +30,71 @@ const moodScores: Record<string, number> = {
};
export default function DashboardPage() {
const journals =
useLiveQuery(() => db.journals.orderBy("date").toArray()) || [];
const tasks = useLiveQuery(() => db.tasks.toArray()) || [];
const liveJournals = useLiveQuery<Journal[]>(() =>
db.journals.orderBy("date").toArray(),
);
const liveTasks = useLiveQuery<Task[]>(() => db.tasks.toArray());
const pendingTasksCount = tasks.filter((t) => t.status === "todo").length;
const journals = liveJournals ?? EMPTY_JOURNALS;
const tasks = liveTasks ?? EMPTY_TASKS;
const pendingTasksCount = tasks.filter((task) => task.status === "todo").length;
// Bugüne ait enerji seviyesi
const today = new Date().toISOString().split("T")[0];
const todaysJournal = journals.find((j) => j.date === today);
const todaysJournal = journals.find((journal) => journal.date === today);
const todaysEnergy = todaysJournal ? todaysJournal.energy : "--";
// 1. Grafik: Son 7 günün Ruh Hali ve Enerji trendi
const trendData = useMemo(() => {
const dataMap: Record<
string,
{ date: string; moodSum: number; energySum: number; count: number }
> = {};
journals.forEach((j) => {
if (!dataMap[j.date]) {
dataMap[j.date] = { date: j.date, moodSum: 0, energySum: 0, count: 0 };
journals.forEach((journal) => {
if (!dataMap[journal.date]) {
dataMap[journal.date] = {
date: journal.date,
moodSum: 0,
energySum: 0,
count: 0,
};
}
dataMap[j.date].moodSum += moodScores[j.mood] || 3;
dataMap[j.date].energySum += j.energy;
dataMap[j.date].count += 1;
dataMap[journal.date].moodSum += moodScores[journal.mood] || 3;
dataMap[journal.date].energySum += journal.energy;
dataMap[journal.date].count += 1;
});
return Object.values(dataMap)
.map((d) => ({
date: d.date.slice(5), // Sadece MM-DD formatı alalım
Mood: Number((d.moodSum / d.count).toFixed(1)),
Enerji: Number((d.energySum / d.count).toFixed(1)),
.map((entry) => ({
date: entry.date.slice(5),
Mood: Number((entry.moodSum / entry.count).toFixed(1)),
Enerji: Number((entry.energySum / entry.count).toFixed(1)),
}))
.slice(-7); // Sadece son 7 günü göster
.slice(-7);
}, [journals]);
// 2. Grafik: En çok kullanılan AI etiketleri
const tagData = useMemo(() => {
const counts: Record<string, number> = {};
journals.forEach((j) => {
if (j.ai_tags) {
j.ai_tags.forEach((tag) => {
counts[tag] = (counts[tag] || 0) + 1;
});
}
journals.forEach((journal) => {
journal.ai_tags?.forEach((tag) => {
counts[tag] = (counts[tag] || 0) + 1;
});
});
return Object.entries(counts)
.map(([name, value]) => ({ name, Değer: value }))
.sort((a, b) => b.Değer - a.Değer)
.slice(0, 5); // En çok geçen 5 etiket
.slice(0, 5);
}, [journals]);
// Son günlüğe ait AI Summary
const lastInsight = [...journals]
.reverse()
.find((j) => j.ai_summary)?.ai_summary;
.find((journal) => journal.ai_summary)?.ai_summary;
return (
<div className="space-y-6 max-w-6xl mx-auto animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="mx-auto max-w-6xl animate-in space-y-6 fade-in slide-in-from-bottom-4 duration-500">
<div className="flex flex-col gap-2">
<h1 className="text-3xl font-bold tracking-tight">Bugün Nasılsın?</h1>
<p className="text-muted-foreground">
@@ -94,15 +104,13 @@ export default function DashboardPage() {
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
<CardTitle className="text-sm font-medium">
Günlük Kayıtları
</CardTitle>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Günlük Kayıtları</CardTitle>
<PenTool className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{journals.length}</div>
<p className="text-xs text-muted-foreground mt-1">
<p className="mt-1 text-xs text-muted-foreground">
{journals.length === 0
? "Henüz kayıt girilmedi"
: "Toplam kayıt eklendi"}
@@ -111,58 +119,49 @@ export default function DashboardPage() {
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
<CardTitle className="text-sm font-medium">
Aktif Görevler
</CardTitle>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Aktif Görevler</CardTitle>
<CheckCircle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{pendingTasksCount}</div>
<p className="text-xs text-muted-foreground mt-1">Bekleyen görev</p>
<p className="mt-1 text-xs text-muted-foreground">Bekleyen görev</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
<CardTitle className="text-sm font-medium">
Bugünkü Enerji
</CardTitle>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Bugünkü Enerji</CardTitle>
<Activity className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{todaysEnergy}</div>
<p className="text-xs text-muted-foreground mt-1">
{todaysJournal ? "/ 5 Seviyesinde" : "Kayıt bekleniyor"}
<p className="mt-1 text-xs text-muted-foreground">
{todaysJournal ? "/ 5 seviyesinde" : "Kayıt bekleniyor"}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
<CardTitle className="text-sm font-medium">
Local AI Durumu
</CardTitle>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Local AI Durumu</CardTitle>
<Brain className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-500">Hazır</div>
<p className="text-xs text-muted-foreground mt-1">
<p className="mt-1 text-xs text-muted-foreground">
Ollama / Veri bekliyor
</p>
</CardContent>
</Card>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7 ">
{/* Line Chart */}
<Card className="lg:col-span-4 p-6 flex flex-col justify-between">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
<Card className="flex flex-col justify-between p-6 lg:col-span-4">
<div className="mb-4">
<h3 className="text-lg font-medium mb-1">
Ruh Hali & Enerji Trendi
</h3>
<h3 className="mb-1 text-lg font-medium">Ruh Hali ve Enerji Trendi</h3>
<p className="text-sm text-muted-foreground">
Son 7 günlük ortalamalar (1-5 Arası Puanlama)
Son 7 günlük ortalamalar (1-5 arası puanlama)
</p>
</div>
<div className="h-[250px] w-full">
@@ -196,9 +195,7 @@ export default function DashboardPage() {
}}
itemStyle={{ fontWeight: "500" }}
/>
<Legend
wrapperStyle={{ paddingTop: "10px", fontSize: "14px" }}
/>
<Legend wrapperStyle={{ paddingTop: "10px", fontSize: "14px" }} />
<Line
type="monotone"
dataKey="Mood"
@@ -218,32 +215,25 @@ export default function DashboardPage() {
</LineChart>
</ResponsiveContainer>
) : (
<div className="h-full flex items-center justify-center border-2 border-dashed rounded-md bg-muted/20">
<p className="text-sm text-muted-foreground">
Veri bekleniyor...
</p>
<div className="flex h-full items-center justify-center rounded-md border-2 border-dashed bg-muted/20">
<p className="text-sm text-muted-foreground">Veri bekleniyor...</p>
</div>
)}
</div>
</Card>
{/* Bar Chart & Insight */}
<div className="lg:col-span-3 space-y-4 flex flex-col">
<Card className="p-6 flex-1 flex flex-col">
<div className="flex flex-col space-y-4 lg:col-span-3">
<Card className="flex flex-1 flex-col p-6">
<div className="mb-4">
<h3 className="text-lg font-medium mb-1">AI Konu Dağılımı</h3>
<h3 className="mb-1 text-lg font-medium">AI Konu Dağılımı</h3>
<p className="text-sm text-muted-foreground">
Günlüklerinden çıkarılan en sık 5 etiket
</p>
</div>
<div className="h-[150px] w-full mt-auto">
<div className="mt-auto h-[150px] w-full">
{tagData.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={tagData}
layout="vertical"
margin={{ left: -20 }}
>
<BarChart data={tagData} layout="vertical" margin={{ left: -20 }}>
<CartesianGrid
strokeDasharray="3 3"
horizontal={false}
@@ -276,20 +266,19 @@ export default function DashboardPage() {
</BarChart>
</ResponsiveContainer>
) : (
<div className="h-full flex items-center justify-center border-2 border-dashed rounded-md bg-muted/20">
<p className="text-sm text-muted-foreground">
Yeterli etiket yok.
</p>
<div className="flex h-full items-center justify-center rounded-md border-2 border-dashed bg-muted/20">
<p className="text-sm text-muted-foreground">Yeterli etiket yok.</p>
</div>
)}
</div>
</Card>
<Card className="p-6 bg-primary/5 border-primary/20 flex-1">
<h3 className="text-lg font-medium mb-3 flex items-center gap-2">
<Brain className="w-5 h-5 text-primary" /> Son AI İçgörüsü
<Card className="flex-1 border-primary/20 bg-primary/5 p-6">
<h3 className="mb-3 flex items-center gap-2 text-lg font-medium">
<Brain className="h-5 w-5 text-primary" />
Son AI İçgörüsü
</h3>
<p className="text-sm text-foreground/80 leading-relaxed italic">
<p className="text-sm italic leading-relaxed text-foreground/80">
{lastInsight
? `"${lastInsight}"`
: "Henüz bir içgörü oluşmadı. Biraz günlük yaz, AI analiz yapsın."}
+32 -25
View File
@@ -1,12 +1,22 @@
'use server'
import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'
import { createClient } from '@/lib/supabase/server'
type ProfileUpdateData = {
first_name: string
last_name: string
avatar_url?: string
}
export async function updateProfile(formData: FormData) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
const {
data: { user },
} = await supabase.auth.getUser()
if (!user) {
return { error: 'Kullanıcı bulunamadı.' }
}
@@ -15,46 +25,45 @@ export async function updateProfile(formData: FormData) {
const lastName = formData.get('lastName') as string
const avatarFile = formData.get('avatar') as File | null
let avatarUrl = undefined
let avatarUrl: string | undefined
// Upload avatar if a new file is provided
if (avatarFile && avatarFile.size > 0) {
const fileExt = avatarFile.name.split('.').pop()
const fileName = `${user.id}/${Math.random()}.${fileExt}`
const { error: uploadError, data: uploadData } = await supabase.storage
const { error: uploadError } = await supabase.storage
.from('avatars')
.upload(fileName, avatarFile, { upsert: true })
if (uploadError) {
return { error: 'Profil fotoğrafı yüklenirken hata oluştu: ' + uploadError.message }
return {
error: `Profil fotoğrafı yüklenirken hata oluştu: ${uploadError.message}`,
}
}
// Get public URL
const { data: { publicUrl } } = supabase.storage
.from('avatars')
.getPublicUrl(fileName)
const {
data: { publicUrl },
} = supabase.storage.from('avatars').getPublicUrl(fileName)
avatarUrl = publicUrl
}
// Update profile
const updateData: any = {
const updateData: ProfileUpdateData = {
first_name: firstName,
last_name: lastName,
}
if (avatarUrl) {
updateData.avatar_url = avatarUrl
}
// Upsert profile in case it doesn't exist yet
const { error } = await supabase
.from('profiles')
.upsert({ id: user.id, ...updateData })
const { error } = await supabase.from('profiles').upsert({
id: user.id,
...updateData,
})
if (error) {
return { error: 'Profil güncellenirken hata oluştu: ' + error.message }
return { error: `Profil güncellenirken hata oluştu: ${error.message}` }
}
revalidatePath('/settings')
@@ -64,17 +73,15 @@ export async function updateProfile(formData: FormData) {
export async function updatePassword(formData: FormData) {
const supabase = await createClient()
const password = formData.get('password') as string
if (!password || password.length < 6) {
return { error: 'Şifre en az 6 karakter olmalıdır.' }
}
const { error } = await supabase.auth.updateUser({
password: password
})
const { error } = await supabase.auth.updateUser({ password })
if (error) {
return { error: 'Şifre güncellenirken hata oluştu: ' + error.message }
return { error: `Şifre güncellenirken hata oluştu: ${error.message}` }
}
return { success: true }
+160 -81
View File
@@ -1,137 +1,201 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import { Bot, KeyRound, Save, User } from "lucide-react";
import { updatePassword, updateProfile } from "./actions";
import { createClient } from "@/lib/supabase/client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Bot, Save, User, KeyRound } from "lucide-react";
import { createClient } from "@/lib/supabase/client";
import { updateProfile, updatePassword } from "./actions";
type AiProvider = "groq" | "ollama" | "openai";
function getInitialAiProvider(): AiProvider {
if (typeof window === "undefined") {
return "groq";
}
const savedProvider = localStorage.getItem("mindspace_ai_provider");
return savedProvider === "ollama" || savedProvider === "openai"
? savedProvider
: "groq";
}
function getInitialApiKey() {
if (typeof window === "undefined") {
return "";
}
return localStorage.getItem("mindspace_api_key") ?? "";
}
export default function SettingsPage() {
const [aiProvider, setAiProvider] = useState("groq");
const [apiKey, setApiKey] = useState("");
const [aiProvider, setAiProvider] = useState<AiProvider>(getInitialAiProvider);
const [apiKey, setApiKey] = useState(getInitialApiKey);
const [saveStatus, setSaveStatus] = useState("");
// Profile state
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [avatarUrl, setAvatarUrl] = useState("");
const [profileSaveStatus, setProfileSaveStatus] = useState("");
const [passwordSaveStatus, setPasswordSaveStatus] = useState("");
const [supabase] = useState(() => createClient());
const formRef = useRef<HTMLFormElement>(null);
const supabase = createClient();
useEffect(() => {
const savedProvider = localStorage.getItem("mindspace_ai_provider");
const savedApiKey = localStorage.getItem("mindspace_api_key");
if (savedProvider) setAiProvider(savedProvider);
if (savedApiKey) setApiKey(savedApiKey);
// Fetch user profile
async function fetchProfile() {
const { data: { user } } = await supabase.auth.getUser();
if (user) {
const { data } = await supabase.from("profiles").select("*").eq("id", user.id).single();
if (data) {
setFirstName(data.first_name || "");
setLastName(data.last_name || "");
setAvatarUrl(data.avatar_url || "");
}
let isActive = true;
const fetchProfile = async () => {
const {
data: { user },
} = await supabase.auth.getUser();
if (!user || !isActive) {
return;
}
}
fetchProfile();
}, []);
const { data } = await supabase
.from("profiles")
.select("*")
.eq("id", user.id)
.single();
if (!data || !isActive) {
return;
}
setFirstName(data.first_name || "");
setLastName(data.last_name || "");
setAvatarUrl(data.avatar_url || "");
};
void fetchProfile();
return () => {
isActive = false;
};
}, [supabase]);
const handleSaveAI = () => {
localStorage.setItem("mindspace_ai_provider", aiProvider);
localStorage.setItem("mindspace_api_key", apiKey);
setSaveStatus("Ayarlar başarıyla kaydedildi!");
setTimeout(() => setSaveStatus(""), 3000);
window.setTimeout(() => setSaveStatus(""), 3000);
};
const handleProfileAction = async (formData: FormData) => {
const res = await updateProfile(formData);
if (res?.error) {
setProfileSaveStatus("Hata: " + res.error);
const response = await updateProfile(formData);
if (response?.error) {
setProfileSaveStatus(`Hata: ${response.error}`);
} else {
setProfileSaveStatus("Profil başarıyla güncellendi!");
if (formData.get("avatar") && (formData.get("avatar") as File).size > 0) {
const avatar = formData.get("avatar");
if (avatar instanceof File && avatar.size > 0) {
window.location.reload();
}
}
setTimeout(() => setProfileSaveStatus(""), 3000);
window.setTimeout(() => setProfileSaveStatus(""), 3000);
};
const handlePasswordAction = async (formData: FormData) => {
const res = await updatePassword(formData);
if (res?.error) {
setPasswordSaveStatus("Hata: " + res.error);
const response = await updatePassword(formData);
if (response?.error) {
setPasswordSaveStatus(`Hata: ${response.error}`);
} else {
setPasswordSaveStatus("Şifre başarıyla güncellendi!");
formRef.current?.reset();
}
setTimeout(() => setPasswordSaveStatus(""), 3000);
window.setTimeout(() => setPasswordSaveStatus(""), 3000);
};
return (
<div className="flex flex-col gap-6 p-4 max-w-2xl mx-auto w-full">
<div className="mx-auto flex w-full max-w-2xl flex-col gap-6 p-4">
<div>
<h1 className="text-3xl font-bold text-foreground">Ayarlar</h1>
<p className="text-muted-foreground mt-1">
<p className="mt-1 text-muted-foreground">
Kullanıcı profili ve yapay zeka asistanı yapılandırmanızı yönetin.
</p>
</div>
{/* Profile Settings */}
<div className="bg-card border border-border rounded-xl p-6 shadow-sm space-y-6 flex flex-col">
<div className="flex flex-col space-y-6 rounded-xl border border-border bg-card p-6 shadow-sm">
<div className="flex items-center gap-3 border-b border-border/50 pb-4">
<User className="w-6 h-6 text-primary" />
<User className="h-6 w-6 text-primary" />
<h2 className="text-xl font-semibold">Kullanıcı Profili</h2>
</div>
<form action={handleProfileAction} className="space-y-4">
<div className="flex items-center gap-4 mb-6">
<div className="mb-6 flex items-center gap-4">
{avatarUrl ? (
<img src={avatarUrl} alt="Avatar" className="w-16 h-16 rounded-full object-cover border border-border" />
<>
{/* Avatar URL dış kaynaklı olduğu için bu önizlemede native img kullanıyoruz. */}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={avatarUrl}
alt="Avatar"
className="h-16 w-16 rounded-full border border-border object-cover"
/>
</>
) : (
<div className="w-16 h-16 rounded-full bg-muted flex items-center justify-center border border-border">
<User className="w-8 h-8 text-muted-foreground" />
</div>
<div className="flex h-16 w-16 items-center justify-center rounded-full border border-border bg-muted">
<User className="h-8 w-8 text-muted-foreground" />
</div>
)}
<div className="space-y-1 flex-1">
<div className="flex-1 space-y-1">
<Label htmlFor="avatar">Profil Fotoğrafı Yükle</Label>
<Input id="avatar" name="avatar" type="file" accept="image/*" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="firstName">Ad</Label>
<Input id="firstName" name="firstName" value={firstName} onChange={(e) => setFirstName(e.target.value)} />
<Input
id="firstName"
name="firstName"
value={firstName}
onChange={(event) => setFirstName(event.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="lastName">Soyad</Label>
<Input id="lastName" name="lastName" value={lastName} onChange={(e) => setLastName(e.target.value)} />
<Input
id="lastName"
name="lastName"
value={lastName}
onChange={(event) => setLastName(event.target.value)}
/>
</div>
</div>
<div className="flex items-center gap-4 mt-4">
<div className="mt-4 flex items-center gap-4">
<Button type="submit" className="w-max">
<Save className="w-4 h-4 mr-2" />
<Save className="mr-2 h-4 w-4" />
Profili Kaydet
</Button>
{profileSaveStatus && (
<span className={`text-sm ${profileSaveStatus.startsWith("Hata") ? "text-red-500" : "text-green-600 dark:text-green-400"}`}>
<span
className={`text-sm ${
profileSaveStatus.startsWith("Hata")
? "text-red-500"
: "text-green-600 dark:text-green-400"
}`}
>
{profileSaveStatus}
</span>
)}
@@ -139,26 +203,38 @@ export default function SettingsPage() {
</form>
</div>
{/* Password Settings */}
<div className="bg-card border border-border rounded-xl p-6 shadow-sm space-y-6 flex flex-col">
<div className="flex flex-col space-y-6 rounded-xl border border-border bg-card p-6 shadow-sm">
<div className="flex items-center gap-3 border-b border-border/50 pb-4">
<KeyRound className="w-6 h-6 text-primary" />
<KeyRound className="h-6 w-6 text-primary" />
<h2 className="text-xl font-semibold">Şifre Değiştir</h2>
</div>
<form ref={formRef} action={handlePasswordAction} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="password">Yeni Şifre</Label>
<Input id="password" name="password" type="password" minLength={6} placeholder="En az 6 karakter" required />
<Input
id="password"
name="password"
type="password"
minLength={6}
placeholder="En az 6 karakter"
required
/>
</div>
<div className="flex items-center gap-4 mt-4">
<div className="mt-4 flex items-center gap-4">
<Button type="submit" className="w-max">
<Save className="w-4 h-4 mr-2" />
<Save className="mr-2 h-4 w-4" />
Şifreyi Güncelle
</Button>
{passwordSaveStatus && (
<span className={`text-sm ${passwordSaveStatus.startsWith("Hata") ? "text-red-500" : "text-green-600 dark:text-green-400"}`}>
<span
className={`text-sm ${
passwordSaveStatus.startsWith("Hata")
? "text-red-500"
: "text-green-600 dark:text-green-400"
}`}
>
{passwordSaveStatus}
</span>
)}
@@ -166,31 +242,34 @@ export default function SettingsPage() {
</form>
</div>
{/* AI Settings */}
<div className="bg-card border border-border rounded-xl p-6 shadow-sm space-y-6 flex flex-col">
<div className="flex flex-col space-y-6 rounded-xl border border-border bg-card p-6 shadow-sm">
<div className="flex items-center gap-3 border-b border-border/50 pb-4">
<Bot className="w-6 h-6 text-primary" />
<Bot className="h-6 w-6 text-primary" />
<h2 className="text-xl font-semibold">Terapist (AI) Ayarları</h2>
</div>
<div className="space-y-4">
<div className="space-y-2">
<Label>AI Sağlayıcısı</Label>
<Select value={aiProvider} onValueChange={setAiProvider}>
<Select
value={aiProvider}
onValueChange={(value) => setAiProvider(value as AiProvider)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Sağlayıcı seçin" />
</SelectTrigger>
<SelectContent>
<SelectItem value="ollama">
Ollama (Yerel & Gizlilik Odaklı)
Ollama (Yerel ve Gizlilik Odaklı)
</SelectItem>
<SelectItem value="openai">OpenAI (GPT-4o vb.)</SelectItem>
<SelectItem value="groq">Groq (Llama-3 Bulut)</SelectItem>
<SelectItem value="groq">Groq (Llama 3 Bulut)</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
Gizliliğiniz için yerel Ollama önerilir. Sunucunuzda çalışmayan
durumlarda OpenAI veya Groq gibi bulut çözümlerine geçebilirsiniz.
durumlarda OpenAI veya Groq gibi bulut çözümlerine
geçebilirsiniz.
</p>
</div>
@@ -200,19 +279,19 @@ export default function SettingsPage() {
<Input
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
onChange={(event) => setApiKey(event.target.value)}
placeholder="sk-..."
/>
<p className="text-xs text-muted-foreground">
Anahtarınız sadece tarayıcınızın kendi local hafızasında güvenle
saklanır, hiçbir sunucuya kaydedilmez.
Anahtarınız sadece tarayıcınızın kendi local hafızasında saklanır;
herhangi bir sunucuya kaydedilmez.
</p>
</div>
)}
<div className="flex items-center gap-4 mt-4">
<Button onClick={handleSaveAI} className="w-max">
<Save className="w-4 h-4 mr-2" />
<div className="mt-4 flex items-center gap-4">
<Button type="button" onClick={handleSaveAI} className="w-max">
<Save className="mr-2 h-4 w-4" />
Kaydet
</Button>
{saveStatus && (
+73 -24
View File
@@ -1,14 +1,53 @@
import { NextResponse } from "next/server";
type ChatProvider = "groq" | "openai" | "ollama";
type ChatRequestBody = {
provider?: ChatProvider;
apiKey?: string;
userMessageContent?: string;
};
type ProviderErrorResponse = {
error?: {
message?: string;
};
};
type ChatCompletionResponse = {
choices?: Array<{
message?: {
content?: string;
};
}>;
};
type OllamaResponse = {
response?: string;
};
function getErrorMessage(error: unknown) {
return error instanceof Error ? error.message : "Bilinmeyen sunucu hatası";
}
export async function POST(request: Request) {
try {
const body = await request.json();
const { provider, apiKey, userMessageContent } = body;
const body = (await request.json()) as ChatRequestBody;
const provider = body.provider ?? "ollama";
const apiKey = body.apiKey ?? "";
const userMessageContent = body.userMessageContent?.trim();
if (!userMessageContent) {
return NextResponse.json(
{ error: "Mesaj içeriği boş olamaz." },
{ status: 400 },
);
}
let assistantReply = "";
if (provider === "groq") {
const res = await fetch(
const response = await fetch(
"https://api.groq.com/openai/v1/chat/completions",
{
method: "POST",
@@ -30,14 +69,15 @@ export async function POST(request: Request) {
},
);
if (!res.ok) {
const errorData = await res.json();
throw new Error(errorData.error?.message || "Groq API Hatası");
if (!response.ok) {
const errorData = (await response.json()) as ProviderErrorResponse;
throw new Error(errorData.error?.message || "Groq API hatası");
}
const data = await res.json();
assistantReply = data.choices[0].message.content;
const data = (await response.json()) as ChatCompletionResponse;
assistantReply = data.choices?.[0]?.message?.content ?? "";
} else if (provider === "openai") {
const res = await fetch("https://api.openai.com/v1/chat/completions", {
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -55,32 +95,41 @@ export async function POST(request: Request) {
],
}),
});
if (!res.ok) throw new Error("OpenAI API Hatası");
const data = await res.json();
assistantReply = data.choices[0].message.content;
if (!response.ok) {
throw new Error("OpenAI API hatası");
}
const data = (await response.json()) as ChatCompletionResponse;
assistantReply = data.choices?.[0]?.message?.content ?? "";
} else {
// Varsayılan: Yerel Ollama
const res = await fetch("http://127.0.0.1:11434/api/generate", {
const response = await fetch("http://127.0.0.1:11434/api/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "llama3", // veya mistral
model: "llama3",
prompt: `Sen MindSpace adlı kullanıcının kişisel yapay zeka terapistisin ve sırdaşısın. Şefkatli ve destekleyici cevap ver.\n\nKullanıcı: ${userMessageContent}\nTerapist:`,
stream: false,
}),
});
if (!res.ok) throw new Error("Ollama API yanıt vermedi.");
const data = await res.json();
assistantReply = data.response;
if (!response.ok) {
throw new Error("Ollama API yanıt vermedi.");
}
const data = (await response.json()) as OllamaResponse;
assistantReply = data.response ?? "";
}
if (!assistantReply) {
throw new Error("Model geçerli bir yanıt üretmedi.");
}
return NextResponse.json({ reply: assistantReply });
} catch (error: any) {
console.error("API Route Hatası:", error);
return NextResponse.json(
{ error: error.message || "Bilinmeyen Sunucu Hatası" },
{ status: 500 },
);
} catch (error: unknown) {
const message = getErrorMessage(error);
console.error("API route hatası:", error);
return NextResponse.json({ error: message }, { status: 500 });
}
}
+9
View File
@@ -39,3 +39,12 @@ export async function signup(formData: FormData) {
revalidatePath('/', 'layout')
redirect('/')
}
export async function signOut() {
const supabase = await createClient()
await supabase.auth.signOut()
revalidatePath('/', 'layout')
redirect('/login')
}