new components

This commit is contained in:
k
2026-09-24 13:06:10 +02:00
parent 67f13dbc93
commit 2dab720e55
7 changed files with 296 additions and 32 deletions
+4 -3
View File
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react";
import { Archive, ArchiveRestore, BookOpen, Bot, Check, Copy, Cpu, FileText, Flag, Menu, Mic, PanelLeftOpen, Paperclip, RotateCcw, ScanText, Search, SendHorizontal, Share2, Square, User as UserIcon, Volume2, VolumeX, X } from "lucide-react"; import { Archive, ArchiveRestore, BookOpen, Bot, Check, Copy, Cpu, FileText, Flag, Menu, Mic, PanelLeftOpen, Paperclip, RotateCcw, ScanText, Search, SendHorizontal, Share2, Square, User as UserIcon, Volume2, VolumeX, X } from "lucide-react";
import type { Attachment, ChatMessage, Conversation, DriverModel, User } from "../lib/types.ts"; import type { Attachment, ChatMessage, Conversation, DriverModel, User } from "../lib/types.ts";
import { api, ApiError, type ReportReason } from "../lib/api.ts"; import { api, ApiError, type ReportReason } from "../lib/api.ts";
import { AssistantAvatar, Avatar, Button, Field, FlowerMark, IconButton, Modal, Picker, type PickerGroup, Spinner } from "./ui.tsx"; import { AssistantAvatar, Avatar, Button, copyText, Field, FlowerMark, IconButton, Modal, Picker, type PickerGroup, Spinner } from "./ui.tsx";
import { Markdown } from "./Markdown.tsx"; import { Markdown } from "./Markdown.tsx";
import { UserMessageContent } from "./MessageContent.tsx"; import { UserMessageContent } from "./MessageContent.tsx";
import { cn } from "../lib/cn.ts"; import { cn } from "../lib/cn.ts";
@@ -17,10 +17,11 @@ function MessageBubble({ msg, index, authorAvatar, authorName, onReport, reports
const isUser = msg.role === "user"; const isUser = msg.role === "user";
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
function copy() { function copy() {
navigator.clipboard.writeText(msg.content).then(() => { copyText(msg.content).then((ok) => {
if (!ok) return;
setCopied(true); setCopied(true);
setTimeout(() => setCopied(false), 1500); setTimeout(() => setCopied(false), 1500);
}).catch(() => {}); });
} }
function searchWeb() { function searchWeb() {
window.open(`https://duckduckgo.com/?q=${encodeURIComponent(msg.content.slice(0, 400))}`, "_blank", "noopener,noreferrer"); window.open(`https://duckduckgo.com/?q=${encodeURIComponent(msg.content.slice(0, 400))}`, "_blank", "noopener,noreferrer");
+13 -2
View File
@@ -4,6 +4,16 @@ import remarkGfm from "remark-gfm";
import { CopyButton } from "./ui.tsx"; import { CopyButton } from "./ui.tsx";
import { highlightCode } from "../lib/highlight.tsx"; import { highlightCode } from "../lib/highlight.tsx";
/** Raw source text of a react-markdown <code> element (children may be split). */
export function extractCodeText(children: unknown): string {
if (children === null || children === undefined) return "";
if (typeof children === "string" || typeof children === "number") return String(children);
if (Array.isArray(children)) return children.map(extractCodeText).join("");
const props = (children as { props?: { children?: unknown } })?.props;
if (props && typeof props === "object" && "children" in props) return extractCodeText(props.children);
return "";
}
function CodeBlock({ code, lang }: { code: string; lang: string }) { function CodeBlock({ code, lang }: { code: string; lang: string }) {
const nodes = useMemo(() => highlightCode(code, lang), [code, lang]); const nodes = useMemo(() => highlightCode(code, lang), [code, lang]);
return ( return (
@@ -31,9 +41,10 @@ export function Markdown({ text }: { text: string }) {
// react-markdown nests <code> inside <pre>; render our block instead. // react-markdown nests <code> inside <pre>; render our block instead.
const child = Array.isArray(children) ? children[0] : children; const child = Array.isArray(children) ? children[0] : children;
const props = (child as { props?: { className?: string; children?: unknown } })?.props; const props = (child as { props?: { className?: string; children?: unknown } })?.props;
const className = props?.className || ""; if (!props || typeof props !== "object") return <pre>{children}</pre>;
const className = typeof props.className === "string" ? props.className : "";
const lang = className.replace("language-", ""); const lang = className.replace("language-", "");
const code = String(props?.children ?? "").replace(/\n$/, ""); const code = extractCodeText(props.children).replace(/\n$/, "");
return <CodeBlock code={code} lang={lang} />; return <CodeBlock code={code} lang={lang} />;
}, },
code({ children }) { code({ children }) {
+205 -24
View File
@@ -1,9 +1,9 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { Archive, ArchiveRestore, Eye, Fingerprint, Globe, FileText, ImagePlus, KeyRound, Link2, Mail, Mic, Palette, Pencil, Play, Plug, ScanText, Search, Settings2, Share2, ShieldCheck, Sparkles, Square, Tag, Trash2, Unplug, User as UserIcon, Volume2, X } from "lucide-react"; import { Archive, ArchiveRestore, Eye, Fingerprint, Globe, FileText, ImagePlus, KeyRound, Link2, Mail, Mic, Palette, Pencil, Pipette, Play, Plug, ScanText, Search, Settings2, Share2, ShieldCheck, Sparkles, Square, Tag, Trash2, Unplug, User as UserIcon, Volume2, X } from "lucide-react";
import { api, type UserProvider, type VoicePreview } from "../lib/api.ts"; import { api, type UserProvider, type VoicePreview } from "../lib/api.ts";
import { pushToast } from "../lib/toasts.ts"; import { pushToast } from "../lib/toasts.ts";
import { cn } from "../lib/cn.ts"; import { cn } from "../lib/cn.ts";
import { ACCENT_PRESETS, applyAccent, currentAccent, previewAccent, type AccentState } from "../lib/accent.ts"; import { ACCENT_PRESETS, applyAccent, currentAccent, hexToHsv, hsvToHex, previewAccent, type AccentState } from "../lib/accent.ts";
import { detectLang, getVoicePrefs, listTtsVoices, OPENAI_STATIC_VOICES, pickVoice, playText, setVoicePrefs, stopPlayback, ttsSupported, type VoicePrefs } from "../lib/voice.ts"; import { detectLang, getVoicePrefs, listTtsVoices, OPENAI_STATIC_VOICES, pickVoice, playText, setVoicePrefs, stopPlayback, ttsSupported, type VoicePrefs } from "../lib/voice.ts";
import { FEATURES, setFeature, useFeatures } from "../lib/features.ts"; import { FEATURES, setFeature, useFeatures } from "../lib/features.ts";
import type { Conversation, FilePreview, User } from "../lib/types.ts"; import type { Conversation, FilePreview, User } from "../lib/types.ts";
@@ -794,6 +794,7 @@ function ArchivedSection({ onView, onChanged }: { onView: (c: Conversation) => v
function AccentSection() { function AccentSection() {
const { t } = useT(); const { t } = useT();
const [state, setState] = useState<AccentState>(() => currentAccent()); const [state, setState] = useState<AccentState>(() => currentAccent());
const [pickerOpen, setPickerOpen] = useState(false);
function pick(presetId: string) { function pick(presetId: string) {
const next = { ...state, presetId }; const next = { ...state, presetId };
@@ -802,13 +803,6 @@ function AccentSection() {
pushToast(t("toast.accentChanged"), { icon: "accent", tag: "accent" }); pushToast(t("toast.accentChanged"), { icon: "accent", tag: "accent" });
} }
function pickCustom(hex: string) {
const next = { presetId: "custom", customHex: hex };
setState(next);
previewAccent(hex);
pushToast(t("toast.accentChanged"), { icon: "accent", tag: "accent" });
}
return ( return (
<div> <div>
<span className="mb-1.5 block text-sm font-medium opacity-80">{t("settings.accent")}</span> <span className="mb-1.5 block text-sm font-medium opacity-80">{t("settings.accent")}</span>
@@ -830,26 +824,213 @@ function AccentSection() {
style={{ backgroundColor: p.base }} style={{ backgroundColor: p.base }}
/> />
))} ))}
<label <button
type="button"
onClick={() => setPickerOpen((o) => !o)}
aria-expanded={pickerOpen}
aria-label={t("settings.customAria")}
title={t("settings.customColor")}
className={cn( className={cn(
"relative grid h-8 w-8 cursor-pointer place-items-center overflow-hidden rounded-full border-2 transition active:scale-90", "grid h-8 w-8 place-items-center rounded-full border-2 transition active:scale-90",
state.presetId === "custom" state.presetId === "custom" || pickerOpen
? "border-stone-900 dark:border-white" ? "border-stone-900 dark:border-white"
: "border-transparent hover:scale-110", : "border-transparent hover:scale-110",
)} )}
style={{ background: state.presetId === "custom" ? state.customHex : undefined }} style={{ background: "conic-gradient(#f43f5e,#f59e0b,#84cc16,#14b8a6,#3b82f6,#8b5cf6,#f43f5e)" }}
title={t("settings.customColor")}
> >
<Sparkles size={14} className={cn("text-white drop-shadow-[0_1px_2px_rgba(0,0,0,0.6)]", state.presetId === "custom" && "opacity-0")} /> <Pipette size={14} className="text-white drop-shadow-[0_1px_2px_rgba(0,0,0,0.6)]" />
<input </button>
type="color" </div>
aria-label={t("settings.customAria")} {pickerOpen && (
value={state.presetId === "custom" ? state.customHex : "#10b981"} <AccentCustomPanel
onChange={(e) => pickCustom(e.target.value)} initialHex={state.presetId === "custom" ? state.customHex : (ACCENT_PRESETS.find((p) => p.id === state.presetId)?.base ?? "#10b981")}
onBlur={() => applyAccent(state)} // persist the last previewed value onPreview={(hex) => {
className="absolute inset-0 cursor-pointer opacity-0" setState({ presetId: "custom", customHex: hex });
previewAccent(hex);
}}
onCommit={(hex) => {
const next = { presetId: "custom", customHex: hex };
setState(next);
applyAccent(next);
pushToast(t("toast.accentChanged"), { icon: "accent", tag: "accent" });
}}
/> />
</label> )}
</div>
);
}
/* ---------- Custom accent picker: SV square + hue slider + hex field ---------- */
function AccentCustomPanel({ initialHex, onPreview, onCommit }: {
initialHex: string;
onPreview: (hex: string) => void;
onCommit: (hex: string) => void;
}) {
const { t } = useT();
const start = hexToHsv(initialHex) ?? { h: 160, s: 84, v: 73 };
const [h, setH] = useState(start.h);
const [s, setS] = useState(start.s);
const [v, setV] = useState(start.v);
const [hexInput, setHexInput] = useState(() => hsvToHex(start.h, start.s, start.v));
const [hexError, setHexError] = useState(false);
const [drag, setDrag] = useState<null | "sv" | "hue">(null);
const svRef = useRef<HTMLDivElement>(null);
const hueRef = useRef<HTMLDivElement>(null);
// Synchronous mirror (pointerup fires before setState flushes).
const hsvRef = useRef({ h: start.h, s: start.s, v: start.v });
const currentHex = useMemo(() => hsvToHex(h, s, v), [h, s, v]);
function applyHsv(nh: number, ns: number, nv: number) {
const c = {
h: Math.min(359.99, Math.max(0, nh)),
s: Math.min(100, Math.max(0, ns)),
v: Math.min(100, Math.max(0, nv)),
};
hsvRef.current = c;
setH(c.h);
setS(c.s);
setV(c.v);
const hex = hsvToHex(c.h, c.s, c.v);
setHexInput(hex);
setHexError(false);
onPreview(hex);
}
function posToSv(e: React.PointerEvent): void {
const el = svRef.current;
if (!el) return;
const r = el.getBoundingClientRect();
applyHsv(hsvRef.current.h, ((e.clientX - r.left) / r.width) * 100, (1 - (e.clientY - r.top) / r.height) * 100);
}
function posToHue(e: React.PointerEvent): void {
const el = hueRef.current;
if (!el) return;
const r = el.getBoundingClientRect();
const nh = ((e.clientX - r.left) / r.width) * 360;
const c = hsvRef.current;
applyHsv(nh, c.s, c.v);
}
function begin(kind: "sv" | "hue", e: React.PointerEvent) {
(e.target as HTMLElement).setPointerCapture?.(e.pointerId);
setDrag(kind);
if (kind === "sv") posToSv(e);
else posToHue(e);
}
function commitHex(raw: string) {
const m = /^#?([0-9a-fA-F]{6})$/.exec(raw.trim());
if (!m) {
setHexError(true);
setHexInput(currentHex);
return;
}
const hex = `#${m[1]!.toLowerCase()}`;
const c = hexToHsv(hex);
if (c) {
hsvRef.current = c;
setH(c.h);
setS(c.s);
setV(c.v);
setHexInput(hex);
setHexError(false);
onPreview(hex);
onCommit(hex);
}
}
return (
<div className="mt-3 space-y-3 rounded-2xl border border-stone-200/70 bg-white p-4 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<div
ref={svRef}
role="slider"
aria-label={t("settings.customAria")}
aria-valuetext={currentHex}
tabIndex={0}
onPointerDown={(e) => begin("sv", e)}
onPointerMove={(e) => { if (drag === "sv") posToSv(e); }}
onPointerUp={() => { if (drag === "sv") { setDrag(null); onCommit(hsvToHex(hsvRef.current.h, hsvRef.current.s, hsvRef.current.v)); } }}
onPointerCancel={() => setDrag(null)}
onKeyDown={(e) => {
const step = e.shiftKey ? 10 : 2;
const c = hsvRef.current;
if (e.key === "ArrowLeft") { applyHsv(c.h, c.s - step, c.v); onCommit(hsvToHex(Math.min(359.99, Math.max(0, c.h)), Math.min(100, Math.max(0, c.s - step)), c.v)); }
else if (e.key === "ArrowRight") { applyHsv(c.h, c.s + step, c.v); onCommit(hsvToHex(Math.min(359.99, Math.max(0, c.h)), Math.min(100, Math.max(0, c.s + step)), c.v)); }
else if (e.key === "ArrowUp") { applyHsv(c.h, c.s, c.v + step); onCommit(hsvToHex(Math.min(359.99, Math.max(0, c.h)), c.s, Math.min(100, Math.max(0, c.v + step)))); }
else if (e.key === "ArrowDown") { applyHsv(c.h, c.s, c.v - step); onCommit(hsvToHex(Math.min(359.99, Math.max(0, c.h)), c.s, Math.min(100, Math.max(0, c.v - step)))); }
else return;
e.preventDefault();
}}
className="relative h-40 w-full cursor-crosshair touch-none select-none overflow-hidden rounded-2xl"
style={{ background: `linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, hsl(${h}, 100%, 50%))` }}
>
<span
aria-hidden
className="absolute h-4 w-4 rounded-full border-2 border-white shadow-md"
style={{ left: `${s}%`, top: `${100 - v}%`, transform: "translate(-50%, -50%)", backgroundColor: currentHex }}
/>
</div>
<div
ref={hueRef}
role="slider"
aria-label={t("settings.accent")}
aria-valuetext={String(Math.round(h))}
tabIndex={0}
onPointerDown={(e) => begin("hue", e)}
onPointerMove={(e) => { if (drag === "hue") posToHue(e); }}
onPointerUp={() => { if (drag === "hue") { setDrag(null); onCommit(hsvToHex(hsvRef.current.h, hsvRef.current.s, hsvRef.current.v)); } }}
onPointerCancel={() => setDrag(null)}
onKeyDown={(e) => {
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
const nh = hsvRef.current.h + (e.key === "ArrowRight" ? 4 : -4);
const c = hsvRef.current;
applyHsv(nh, c.s, c.v);
onCommit(hsvToHex(nh, c.s, c.v));
e.preventDefault();
}}
className="relative h-3 w-full cursor-pointer touch-none select-none rounded-full"
style={{ background: "linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00)" }}
>
<span
aria-hidden
className="absolute top-1/2 h-5 w-5 rounded-full border-2 border-white bg-transparent shadow-md"
style={{ left: `calc(${(h / 360) * 100}% - 10px)`, transform: "translateY(-50%)", backgroundColor: currentHex }}
/>
</div>
<div className="flex items-center gap-2">
<span className="h-9 w-9 shrink-0 rounded-full border border-stone-200 dark:border-zinc-700" style={{ backgroundColor: currentHex }} aria-hidden />
<Field label={t("accent.hexLabel")}>
<Input
value={hexInput}
onChange={(e) => {
setHexInput(e.target.value);
const m = /^#?([0-9a-fA-F]{6})$/.exec(e.target.value.trim());
if (m) {
const hex = `#${m[1]!.toLowerCase()}`;
const c = hexToHsv(hex);
if (c) {
hsvRef.current = c;
setH(c.h);
setS(c.s);
setV(c.v);
setHexError(false);
onPreview(hex);
}
} else {
setHexError(e.target.value.trim().length > 0);
}
}}
onBlur={(e) => commitHex(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter") commitHex((e.target as HTMLInputElement).value); }}
maxLength={7}
spellCheck={false}
autoComplete="off"
className={cn("font-mono uppercase", hexError && "!border-red-500")}
/>
</Field>
</div> </div>
</div> </div>
); );
+24 -1
View File
@@ -633,6 +633,28 @@ export function Toggle({
); );
} }
/** Clipboard write with legacy fallback (non-secure contexts lack the async API). */
export async function copyText(text: string): Promise<boolean> {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
try {
const ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
const ok = document.execCommand("copy");
document.body.removeChild(ta);
return ok;
} catch {
return false;
}
}
}
export function CopyButton({ text, label }: { text: string; label?: string }) { export function CopyButton({ text, label }: { text: string; label?: string }) {
const { t } = useT(); const { t } = useT();
const [done, setDone] = useState(false); const [done, setDone] = useState(false);
@@ -640,7 +662,8 @@ export function CopyButton({ text, label }: { text: string; label?: string }) {
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
navigator.clipboard.writeText(text).then(() => { copyText(text).then((ok) => {
if (!ok) return;
setDone(true); setDone(true);
setTimeout(() => setDone(false), 1500); setTimeout(() => setDone(false), 1500);
}); });
+36 -2
View File
@@ -66,8 +66,42 @@ function hslToRgbTriplet(h: number, s: number, l: number): string {
return `${to(r)} ${to(g)} ${to(b)}`; return `${to(r)} ${to(g)} ${to(b)}`;
} }
/** Build the 50–950 ramp from a base hex (HSL ramp, emerald-style). */ /** HSV helpers for the custom color picker (no native input involved). */
export function shadesFor(baseHex: string): AccentShades { export function hexToHsv(hex: string): { h: number; s: number; v: number } | null {
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
if (!m) return null;
const n = parseInt(m[1]!, 16);
const r = ((n >> 16) & 255) / 255, g = ((n >> 8) & 255) / 255, b = (n & 255) / 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
const d = max - min;
let h = 0;
if (d !== 0) {
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) * 60;
else if (max === g) h = ((b - r) / d + 2) * 60;
else h = ((r - g) / d + 4) * 60;
}
return { h, s: max === 0 ? 0 : (d / max) * 100, v: max * 100 };
}
export function hsvToHex(h: number, s: number, v: number): string {
const sn = Math.min(100, Math.max(0, s)) / 100;
const vn = Math.min(100, Math.max(0, v)) / 100;
const hh = ((h % 360) + 360) % 360;
const c = vn * sn;
const x = c * (1 - Math.abs(((hh / 60) % 2) - 1));
const m = vn - c;
let r = 0, g = 0, b = 0;
if (hh < 60) [r, g, b] = [c, x, 0];
else if (hh < 120) [r, g, b] = [x, c, 0];
else if (hh < 180) [r, g, b] = [0, c, x];
else if (hh < 240) [r, g, b] = [0, x, c];
else if (hh < 300) [r, g, b] = [x, 0, c];
else [r, g, b] = [c, 0, x];
const to = (zv: number) => Math.round((zv + m) * 255).toString(16).padStart(2, "0");
return `#${to(r)}${to(g)}${to(b)}`;
}
/** Build the 50–950 ramp from a base hex (HSL ramp, emerald-style). */export function shadesFor(baseHex: string): AccentShades {
const { h, s } = hexToHsl(baseHex); const { h, s } = hexToHsl(baseHex);
const out = {} as AccentShades; const out = {} as AccentShades;
for (const k of SHADE_KEYS) { for (const k of SHADE_KEYS) {
+6
View File
@@ -225,6 +225,7 @@ const en = {
"accent.preset.orange": "Orange", "accent.preset.orange": "Orange",
"accent.preset.teal": "Teal", "accent.preset.teal": "Teal",
"accent.preset.pink": "Pink", "accent.preset.pink": "Pink",
"accent.hexLabel": "Hex color",
"feat.thinking.label": "Thinking", "feat.thinking.label": "Thinking",
"feat.thinking.hint": "feat.thinking.hint":
@@ -948,6 +949,7 @@ const es: Dict = {
"accent.preset.orange": "Naranja", "accent.preset.orange": "Naranja",
"accent.preset.teal": "Turquesa", "accent.preset.teal": "Turquesa",
"accent.preset.pink": "Rosa fuerte", "accent.preset.pink": "Rosa fuerte",
"accent.hexLabel": "Hex color",
"feat.thinking.label": "Razonamiento", "feat.thinking.label": "Razonamiento",
"feat.thinking.hint": "feat.thinking.hint":
@@ -1664,6 +1666,7 @@ const fr: Dict = {
"accent.preset.orange": "Orange", "accent.preset.orange": "Orange",
"accent.preset.teal": "Sarcelle", "accent.preset.teal": "Sarcelle",
"accent.preset.pink": "Rose vif", "accent.preset.pink": "Rose vif",
"accent.hexLabel": "Hex color",
"feat.thinking.label": "Réflexion", "feat.thinking.label": "Réflexion",
"feat.thinking.hint": "Demande au modèle de raisonner étape par étape.", "feat.thinking.hint": "Demande au modèle de raisonner étape par étape.",
@@ -2375,6 +2378,7 @@ const ru: Dict = {
"accent.preset.orange": "Оранжевый", "accent.preset.orange": "Оранжевый",
"accent.preset.teal": "Бирюза", "accent.preset.teal": "Бирюза",
"accent.preset.pink": "Розовый", "accent.preset.pink": "Розовый",
"accent.hexLabel": "Hex color",
"feat.thinking.label": "Размышление", "feat.thinking.label": "Размышление",
"feat.thinking.hint": "Просить модель рассуждать шаг за шагом.", "feat.thinking.hint": "Просить модель рассуждать шаг за шагом.",
@@ -3078,6 +3082,7 @@ const it: Dict = {
"accent.preset.orange": "Arancione", "accent.preset.orange": "Arancione",
"accent.preset.teal": "Teal", "accent.preset.teal": "Teal",
"accent.preset.pink": "Rosa shocking", "accent.preset.pink": "Rosa shocking",
"accent.hexLabel": "Hex color",
"feat.thinking.label": "Ragionamento", "feat.thinking.label": "Ragionamento",
"feat.thinking.hint": "Chiedi al modello di ragionare passo passo.", "feat.thinking.hint": "Chiedi al modello di ragionare passo passo.",
@@ -3784,6 +3789,7 @@ const pt: Dict = {
"accent.preset.orange": "Laranja", "accent.preset.orange": "Laranja",
"accent.preset.teal": "Turquesa", "accent.preset.teal": "Turquesa",
"accent.preset.pink": "Rosa choque", "accent.preset.pink": "Rosa choque",
"accent.hexLabel": "Hex color",
"feat.thinking.label": "Raciocínio", "feat.thinking.label": "Raciocínio",
"feat.thinking.hint": "Pede ao modelo para raciocinar passo a passo.", "feat.thinking.hint": "Pede ao modelo para raciocinar passo a passo.",
+8
View File
@@ -53,3 +53,11 @@ like <model> <button>
---- pouvoir dans admin centers, sur users, envoyez un lien mdp reset à un user ---- pouvoir dans admin centers, sur users, envoyez un lien mdp reset à un user
--- <input type="color" aria-label="Custom accent color" class="absolute inset-0 cursor-pointer opacity-0" value="#10b981">
Ajouter un vrai composant pour pouvoir chosisir la couleur accentique sans dépendre du système.
Utiliser un vrai copiosant come shadcdn.
Avec un vrai accent color picket etpouvoir mettre sontpropr e code hex pour la couleur.