negoptimAi / frontend /src /components /SettingsPanel.tsx
samir12321's picture
Initial commit: Negoptim AI RAG chatbot (backend + frontend + deploy config)
af404c9
Raw
History Blame Contribute Delete
28.5 kB
"use client";
import { useMemo, useState } from "react";
import { Message, TokenUsage, RateLimitInfo } from "@/types/chat";
// ── Groq production models (primary + fallback chain) ───────────────────────
const GROQ_MODEL_CHAIN = [
{ id: "llama-3.1-8b-instant", label: "Llama 3.1 · 8B", note: "Primary — fastest inference" },
{ id: "llama-3.3-70b-versatile", label: "Llama 3.3 · 70B", note: "Fallback 1 — most capable" },
{ id: "openai/gpt-oss-20b", label: "GPT OSS · 20B", note: "Fallback 2 — 1,000 tok/s" },
{ id: "openai/gpt-oss-120b", label: "GPT OSS · 120B", note: "Fallback 3 — strongest on Groq" },
{ id: "qwen/qwen3-32b", label: "Qwen3 · 32B", note: "Fallback 4 — preview" },
];
// ── OpenRouter free models (ordered best → smallest) ────────────────────────
const OPENROUTER_MODELS = [
{ id: "openai/gpt-oss-120b:free", label: "GPT OSS 120B", provider: "OpenAI" },
{ id: "nvidia/nemotron-3-ultra-550b-a55b:free", label: "Nemotron Ultra 550B", provider: "NVIDIA" },
{ id: "nousresearch/hermes-3-llama-3.1-405b:free", label: "Hermes 3 · 405B", provider: "Nous" },
{ id: "nvidia/nemotron-3-super-120b-a12b:free", label: "Nemotron Super 120B", provider: "NVIDIA" },
{ id: "meta-llama/llama-3.3-70b-instruct:free", label: "Llama 3.3 · 70B", provider: "Meta" },
{ id: "qwen/qwen3-next-80b-a3b-instruct:free", label: "Qwen3 · 80B", provider: "Qwen" },
{ id: "qwen/qwen3-coder:free", label: "Qwen3 Coder", provider: "Qwen" },
{ id: "moonshotai/kimi-k2.6:free", label: "Kimi K2.6", provider: "Moonshot" },
{ id: "google/gemma-4-31b-it:free", label: "Gemma 4 · 31B", provider: "Google" },
{ id: "google/gemma-4-26b-a4b-it:free", label: "Gemma 4 · 26B MoE", provider: "Google" },
{ id: "nvidia/nemotron-3-nano-30b-a3b:free", label: "Nemotron Nano 30B", provider: "NVIDIA" },
{ id: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free", label: "Nemotron Nano 30B-R", provider: "NVIDIA" },
{ id: "openai/gpt-oss-20b:free", label: "GPT OSS 20B", provider: "OpenAI" },
{ id: "cognitivecomputations/dolphin-mistral-24b-venice-edition:free", label: "Dolphin Mistral 24B", provider: "CogComp" },
{ id: "nvidia/nemotron-nano-9b-v2:free", label: "Nemotron Nano 9B", provider: "NVIDIA" },
{ id: "meta-llama/llama-3.2-3b-instruct:free", label: "Llama 3.2 · 3B", provider: "Meta" },
{ id: "poolside/laguna-m.1:free", label: "Laguna M.1", provider: "Poolside" },
{ id: "poolside/laguna-xs.2:free", label: "Laguna XS.2", provider: "Poolside" },
{ id: "nex-agi/nex-n2-pro:free", label: "NEX N2 Pro", provider: "NexAGI" },
];
const ALL_MODEL_LABELS: Record<string, string> = {
...Object.fromEntries(GROQ_MODEL_CHAIN.map((m) => [m.id, m.label])),
...Object.fromEntries(OPENROUTER_MODELS.map((m) => [m.id, m.label])),
};
function shortLabel(model: string) {
return ALL_MODEL_LABELS[model] ?? model.split("/").pop()?.replace(/:free$/, "") ?? model;
}
function isOpenRouterModel(id: string) {
return OPENROUTER_MODELS.some((m) => m.id === id);
}
/** Parse Groq reset strings like "58.3s", "3m45.2s", "1h0m0s" → "58s", "3m 45s", "1h" */
function parseReset(reset: string | null | undefined): string {
if (!reset || reset === "0s") return "now";
const m = reset.match(/(?:(\d+)h)?(?:(\d+)m)?(?:([\d.]+)s)?/);
if (!m) return reset;
const h = m[1] ? parseInt(m[1]) : 0;
const min = m[2] ? parseInt(m[2]) : 0;
const s = m[3] ? Math.ceil(parseFloat(m[3])) : 0;
if (h > 0) return min > 0 ? `${h}h ${min}m` : `${h}h`;
if (min > 0) return s > 0 ? `${min}m ${s}s` : `${min}m`;
return s > 0 ? `${s}s` : "now";
}
function pct(remaining: number | null, limit: number | null): number {
if (!limit || remaining === null) return 0;
return Math.max(0, Math.min(100, Math.round((remaining / limit) * 100)));
}
function barColor(remainingPct: number): string {
if (remainingPct > 50) return "from-[#8FD15E] to-[#47C3A6]";
if (remainingPct > 20) return "from-[#F59E0B] to-[#FBBF24]";
return "from-[#EF4444] to-[#F87171]";
}
interface Props {
isOpen: boolean;
onClose: () => void;
messages: Message[];
preferredModel: string;
setPreferredModel: (model: string) => void;
}
export default function SettingsPanel({
isOpen,
onClose,
messages,
preferredModel,
setPreferredModel,
}: Props) {
const [showOpenRouter, setShowOpenRouter] = useState(false);
const [showAdvanced, setShowAdvanced] = useState(false);
const assistantMessages = useMemo(
() => messages.filter((m) => m.role === "assistant" && m.tokenUsage),
[messages]
);
const sessionTotals = useMemo<TokenUsage>(() =>
assistantMessages.reduce(
(acc, m) => ({
prompt_tokens: acc.prompt_tokens + (m.tokenUsage?.prompt_tokens ?? 0),
completion_tokens: acc.completion_tokens + (m.tokenUsage?.completion_tokens ?? 0),
total_tokens: acc.total_tokens + (m.tokenUsage?.total_tokens ?? 0),
}),
{ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
),
[assistantMessages]
);
const latestRateLimit = useMemo<RateLimitInfo | undefined>(() => {
for (let i = assistantMessages.length - 1; i >= 0; i--) {
if (assistantMessages[i].rateLimit) return assistantMessages[i].rateLimit!;
}
return undefined;
}, [assistantMessages]);
const latestTimestamp = useMemo(() => {
if (!assistantMessages.length) return null;
return assistantMessages[assistantMessages.length - 1].timestamp;
}, [assistantMessages]);
const inputRatio = sessionTotals.total_tokens > 0
? Math.round((sessionTotals.prompt_tokens / sessionTotals.total_tokens) * 100)
: 0;
const usedModels = useMemo(() => {
const set = new Set(assistantMessages.map((m) => m.modelUsed).filter(Boolean));
return Array.from(set) as string[];
}, [assistantMessages]);
const reqPct = pct(latestRateLimit?.remaining_requests ?? null, latestRateLimit?.limit_requests ?? null);
const tokPct = pct(latestRateLimit?.remaining_tokens ?? null, latestRateLimit?.limit_tokens ?? null);
const isORActive = isOpenRouterModel(preferredModel);
return (
<>
{/* Backdrop */}
<div
className={`fixed inset-0 z-40 bg-black/20 backdrop-blur-[2px] transition-opacity duration-300 ${
isOpen ? "opacity-100 pointer-events-auto" : "opacity-0 pointer-events-none"
}`}
onClick={onClose}
/>
{/* Drawer */}
<aside className={`fixed top-0 right-0 z-50 h-full w-80 bg-white border-l border-[#E8EDF2] shadow-2xl flex flex-col transition-transform duration-300 ease-in-out ${
isOpen ? "translate-x-0" : "translate-x-full"
}`}>
{/* ── Header ── */}
<div className="flex items-center justify-between px-5 py-4 border-b border-[#E8EDF2]">
<div className="flex items-center gap-2">
<SettingsIcon />
<span className="text-sm font-semibold text-[#1E293B]">Active Session Status</span>
</div>
<button
onClick={onClose}
className="w-7 h-7 flex items-center justify-center rounded-md text-[#94A3B8] hover:text-[#1E293B] hover:bg-[#F1F5F9] transition-colors"
aria-label="Close"
>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
<path d="M2 2l10 10M12 2L2 12" />
</svg>
</button>
</div>
{/* ── Body ── */}
<div className="flex-1 overflow-y-auto">
{/* ── 1. Groq Model Selection ── */}
<section className="px-5 py-5 border-b border-[#F1F5F9]">
<SectionLabel>Groq Models</SectionLabel>
<p className="text-[10px] text-[#94A3B8] mt-1 mb-3 leading-relaxed">
Select the primary Groq model. The remaining models act as automatic fallbacks when rate limits are hit.
</p>
<ol className="space-y-2">
{GROQ_MODEL_CHAIN.map((m) => {
const isPreferred = m.id === preferredModel;
const isRecentlyUsed = usedModels.includes(m.id);
return (
<button
key={m.id}
onClick={() => setPreferredModel(m.id)}
className={`w-full text-left flex items-start gap-3 rounded-lg px-3 py-2.5 border transition-all ${
isPreferred
? "bg-[#F0FDF8] border-[#47C3A6] shadow-sm"
: "bg-[#F8FAFC] border-[#F1F5F9] hover:bg-[#F1F5F9] hover:border-[#E2E8F0]"
}`}
>
<div className="flex flex-col items-center gap-1 pt-0.5 flex-shrink-0">
{isPreferred ? (
<div className="w-4 h-4 rounded-full bg-[#47C3A6] flex items-center justify-center text-white">
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
<polyline points="20 6 9 17 4 12" />
</svg>
</div>
) : (
<div className="w-4 h-4 rounded-full border-2 border-[#CBD5E1] bg-white" />
)}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 flex-wrap">
<span className={`text-[11px] font-semibold ${isPreferred ? "text-[#059669]" : "text-[#334155]"}`}>
{m.label}
</span>
{isPreferred && <Chip color="green">Active</Chip>}
{isRecentlyUsed && !isPreferred && <Chip color="amber">Used</Chip>}
</div>
<p className="text-[10px] text-[#94A3B8] mt-0.5">{m.note}</p>
</div>
</button>
);
})}
</ol>
</section>
{/* ── 2. OpenRouter Free Models ── */}
<section className="border-b border-[#F1F5F9]">
<button
onClick={() => setShowOpenRouter(!showOpenRouter)}
className="w-full flex items-center justify-between px-5 py-4 hover:bg-[#F8FAFC] transition-colors"
>
<div className="flex items-center gap-2">
<div className="w-0.5 h-3.5 rounded-full bg-gradient-to-b from-[#818CF8] to-[#6366F1]" />
<span className="text-[10px] font-bold uppercase tracking-[0.12em] text-[#94A3B8]">
OpenRouter Free Models
</span>
{isORActive && (
<span className="text-[8px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded-full text-[#4F46E5] bg-indigo-50 border border-indigo-200">
Active
</span>
)}
<span className="text-[9px] text-[#CBD5E1] font-mono">
{OPENROUTER_MODELS.length}
</span>
</div>
<svg
width="10" height="6" viewBox="0 0 10 6" fill="none"
stroke="currentColor" strokeWidth="1.5"
className={`text-[#94A3B8] transition-transform duration-200 ${showOpenRouter ? "rotate-180" : ""}`}
>
<path d="M1 1l4 4 4-4" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
{showOpenRouter && (
<div className="px-5 pb-5 pt-1 animate-slide-up">
<p className="text-[10px] text-[#94A3B8] mb-3 leading-relaxed">
Select any free OpenRouter model as primary. Groq models remain as fallback if OpenRouter is unavailable.
</p>
<div className="space-y-1 max-h-64 overflow-y-auto pr-1 -mr-1">
{OPENROUTER_MODELS.map((m) => {
const isActive = m.id === preferredModel;
const isUsed = usedModels.includes(m.id);
return (
<button
key={m.id}
onClick={() => setPreferredModel(m.id)}
className={`w-full text-left flex items-center gap-2.5 rounded-lg px-2.5 py-2 border transition-all ${
isActive
? "bg-indigo-50 border-indigo-300 shadow-sm"
: "bg-[#F8FAFC] border-[#F1F5F9] hover:bg-[#F1F5F9] hover:border-[#E2E8F0]"
}`}
>
{/* Radio */}
{isActive ? (
<div className="w-3.5 h-3.5 rounded-full bg-[#6366F1] flex items-center justify-center flex-shrink-0">
<svg width="7" height="7" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="3.5">
<polyline points="20 6 9 17 4 12" />
</svg>
</div>
) : (
<div className="w-3.5 h-3.5 rounded-full border-2 border-[#CBD5E1] bg-white flex-shrink-0" />
)}
{/* Label */}
<span className={`text-[11px] font-semibold flex-1 min-w-0 truncate ${
isActive ? "text-[#4338CA]" : "text-[#334155]"
}`}>
{m.label}
</span>
{/* Badges */}
<div className="flex items-center gap-1 flex-shrink-0">
{isUsed && !isActive && (
<span className="text-[8px] font-bold px-1 py-0.5 rounded text-amber-600 bg-amber-50 border border-amber-200">
used
</span>
)}
<span className="text-[8px] font-medium px-1.5 py-0.5 rounded bg-[#F1F5F9] text-[#64748B] border border-[#E2E8F0]">
{m.provider}
</span>
</div>
</button>
);
})}
</div>
{/* Current OR selection summary */}
{isORActive && (
<div className="mt-3 rounded-lg bg-indigo-50 border border-indigo-200 px-3 py-2">
<p className="text-[10px] text-[#4338CA] leading-relaxed">
<span className="font-semibold">Active:</span>{" "}
{OPENROUTER_MODELS.find((m) => m.id === preferredModel)?.label}
{" "}via OpenRouter.{" "}
Groq models remain available as fallback.
</p>
</div>
)}
</div>
)}
</section>
{/* ── 3. Live API Quota Metrics ── */}
<section className="px-5 py-5 border-b border-[#F1F5F9]">
<SectionLabel>API Quota & Limits</SectionLabel>
{!latestRateLimit ? (
<div className="mt-3 bg-[#F8FAFC] border border-[#F1F5F9] rounded-lg p-3">
<div className="flex items-center gap-2 mb-1.5">
<div className="w-1.5 h-1.5 rounded-full bg-[#E2E8F0]" />
<span className="text-[11px] font-semibold text-[#64748B]">
Status: Ready (Pending message)
</span>
</div>
<p className="text-[10px] text-[#94A3B8] leading-relaxed">
Remaining requests, token balances, and exact reset times are fetched live from response headers and will display after your first message.
</p>
</div>
) : (
<>
<div className="flex items-center justify-between mt-3 mb-4">
<span className="text-[10px] font-semibold text-[#475569] bg-[#F1F5F9] px-2 py-0.5 rounded-full">
Last response: {shortLabel(latestRateLimit.model)}
</span>
{latestTimestamp && (
<span className="text-[10px] text-[#CBD5E1]">
updated {latestTimestamp.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" })}
</span>
)}
</div>
<div className="space-y-4">
<RateLimitRow
label="Requests / min Quota"
remaining={latestRateLimit.remaining_requests}
limit={latestRateLimit.limit_requests}
reset={latestRateLimit.reset_requests}
pct={reqPct}
/>
<RateLimitRow
label="Tokens / min Quota"
remaining={latestRateLimit.remaining_tokens}
limit={latestRateLimit.limit_tokens}
reset={latestRateLimit.reset_tokens}
pct={tokPct}
formatValue={(n) => n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n)}
/>
</div>
<p className="text-[10px] text-[#CBD5E1] mt-4 leading-relaxed">
Quotas are per-model and independent — switching models gives a fresh quota pool.
</p>
</>
)}
</section>
{/* ── 4. Advanced Token Stats (collapsed) ── */}
<section className="border-b border-[#F1F5F9]">
<button
onClick={() => setShowAdvanced(!showAdvanced)}
className="w-full flex items-center justify-between px-5 py-4 hover:bg-[#F8FAFC] transition-colors"
>
<div className="flex items-center gap-2">
<div className="w-0.5 h-3.5 rounded-full bg-slate-300" />
<span className="text-[10px] font-bold uppercase tracking-[0.12em] text-[#64748B]">
Detailed Metrics
</span>
</div>
<svg
width="10" height="6" viewBox="0 0 10 6" fill="none"
stroke="currentColor" strokeWidth="1.5"
className={`text-[#94A3B8] transition-transform duration-200 ${showAdvanced ? "rotate-180" : ""}`}
>
<path d="M1 1l4 4 4-4" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
{showAdvanced && (
<div className="px-5 pb-5 pt-1 space-y-5 animate-slide-up bg-[#FAFAFB]">
<div>
<span className="text-[10px] font-semibold text-[#94A3B8] uppercase tracking-wider">
Session Totals
</span>
{assistantMessages.length === 0 ? (
<p className="text-xs text-[#94A3B8] mt-2">No token records yet.</p>
) : (
<>
<div className="grid grid-cols-3 gap-2 mt-2">
<StatTile label="Prompt" value={sessionTotals.prompt_tokens} color="#64748B" />
<StatTile label="Completion" value={sessionTotals.completion_tokens} color="#47C3A6" />
<StatTile label="Total" value={sessionTotals.total_tokens} color="#14B7CC" bold />
</div>
<div className="mt-3">
<div className="flex justify-between text-[9px] text-[#94A3B8] mb-1">
<span>Prompt {inputRatio}%</span>
<span>Completion {100 - inputRatio}%</span>
</div>
<div className="h-1.5 rounded-full bg-[#E8EDF2] overflow-hidden">
<div
className="h-full rounded-full bg-gradient-to-r from-[#8FD15E] to-[#47C3A6]"
style={{ width: `${inputRatio}%` }}
/>
</div>
</div>
</>
)}
</div>
{assistantMessages.length > 0 && (
<div>
<span className="text-[10px] font-semibold text-[#94A3B8] uppercase tracking-wider block mb-2">
Prompt History Breakdown
</span>
<div className="space-y-1.5 max-h-48 overflow-y-auto pr-1">
{assistantMessages.map((msg, idx) => {
const usage = msg.tokenUsage!;
const model = msg.modelUsed ?? preferredModel;
const isFallback = model !== preferredModel;
const barWidth = sessionTotals.total_tokens > 0
? Math.max(4, Math.round((usage.total_tokens / sessionTotals.total_tokens) * 100))
: 0;
return (
<div key={msg.id} className="rounded-lg border border-[#E8EDF2] bg-white px-2.5 py-2">
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-1.5">
<span className="text-[9px] font-bold text-[#CBD5E1]">#{idx + 1}</span>
<span className={`text-[9px] font-semibold px-1.5 py-0.2 rounded-full ${
isFallback ? "text-amber-600 bg-amber-50 border border-amber-200" : "text-[#64748B] bg-[#F1F5F9]"
}`}>
{shortLabel(model)}
</span>
</div>
<span className="text-[9px] font-mono font-semibold text-[#334155]">
{usage.total_tokens.toLocaleString()}
</span>
</div>
<div className="flex items-center gap-2">
<div className="flex-1 h-1 bg-[#F1F5F9] rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${isFallback ? "bg-amber-400" : "bg-[#47C3A6]"}`}
style={{ width: `${barWidth}%` }}
/>
</div>
<span className="text-[8px] text-[#94A3B8] font-mono whitespace-nowrap">
↑{usage.prompt_tokens} ↓{usage.completion_tokens}
</span>
</div>
</div>
);
})}
</div>
</div>
)}
</div>
)}
</section>
</div>
{/* ── Footer ── */}
<div className="px-5 py-3 border-t border-[#E8EDF2] bg-[#F8FAFC]">
<p className="text-[10px] text-[#CBD5E1] text-center">
<span className="text-[#94A3B8] font-medium">Groq</span>
{" · "}
<span className="text-[#94A3B8] font-medium">OpenRouter</span>
{" · "}
<span className="text-[#94A3B8]">Free tier limits apply</span>
</p>
</div>
</aside>
</>
);
}
// ── Sub-components ───────────────────────────────────────
function RateLimitRow({
label, remaining, limit, reset, pct: pctValue,
formatValue = (n) => n.toLocaleString(),
}: {
label: string;
remaining: number | null | undefined;
limit: number | null | undefined;
reset: string | null | undefined;
pct: number;
formatValue?: (n: number) => string;
}) {
const color = barColor(pctValue);
const hasData = remaining !== null && remaining !== undefined && limit;
return (
<div>
<div className="flex items-center justify-between mb-1">
<span className="text-[10px] font-semibold text-[#475569] uppercase tracking-wider">{label}</span>
<div className="flex items-center gap-1.5">
{hasData && (
<span className="text-[10px] font-mono text-[#334155]">
<span className={pctValue <= 20 ? "text-red-500 font-bold" : pctValue <= 50 ? "text-amber-500 font-bold" : "text-[#059669] font-bold"}>
{formatValue(remaining!)}
</span>
<span className="text-[#CBD5E1]"> / {formatValue(limit!)}</span>
</span>
)}
{reset && (
<span className="text-[9px] text-[#94A3B8] bg-[#F1F5F9] px-1.5 py-0.5 rounded font-mono">
↺ {parseReset(reset)}
</span>
)}
</div>
</div>
<div className="h-1.5 rounded-full bg-[#F1F5F9] overflow-hidden">
{hasData && (
<div
className={`h-full rounded-full bg-gradient-to-r ${color} transition-all duration-500`}
style={{ width: `${pctValue}%` }}
/>
)}
</div>
{hasData && (
<div className="flex justify-between mt-1">
<span className="text-[8px] text-[#CBD5E1]">{pctValue}% remaining</span>
<span className="text-[8px] text-[#CBD5E1]">resets in {parseReset(reset)}</span>
</div>
)}
</div>
);
}
function SectionLabel({ children }: { children: React.ReactNode }) {
return (
<div className="flex items-center gap-2">
<div className="w-0.5 h-3.5 rounded-full bg-gradient-to-b from-[#47C3A6] to-[#14B7CC]" />
<span className="text-[10px] font-bold uppercase tracking-[0.12em] text-[#94A3B8]">{children}</span>
</div>
);
}
function StatTile({ label, value, color, bold }: { label: string; value: number; color: string; bold?: boolean }) {
return (
<div className="flex flex-col items-center bg-white border border-[#E8EDF2] rounded-lg py-2 px-1">
<span className={`text-sm font-mono tabular-nums leading-none ${bold ? "font-bold" : "font-semibold"}`} style={{ color }}>
{value.toLocaleString()}
</span>
<span className="text-[8px] text-[#CBD5E1] uppercase tracking-wider mt-1">{label}</span>
</div>
);
}
function Chip({ children, color }: { children: React.ReactNode; color: "green" | "amber" }) {
const cls = color === "green"
? "text-[#059669] bg-[#D1FAE5]"
: "text-amber-600 bg-amber-50 border border-amber-200";
return (
<span className={`text-[8px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded-full ${cls}`}>
{children}
</span>
);
}
function SettingsIcon() {
return (
<svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor" className="text-[#94A3B8]">
<path fillRule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clipRule="evenodd" />
</svg>
);
}