AI Prompt Box (Dark Glass)
AI interfaces @bhaskar Stars:0 Views:0
/*
* MIT License
*
* Copyright (c) 2026 OwnlyPlans
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
"use client";
import { useEffect, useRef, useState } from "react";
const css = `.ask{--ask-w:320px;--ask-ink:#f3f5fa;--ask-dim:rgba(255,255,255,.32);--ask-bg:#0d0d0f;
width:min(var(--ask-w),100%);display:grid;gap:12px;font:13px/1.45 system-ui,-apple-system,"Segoe UI",sans-serif;color:var(--ask-ink)}
.ask-thread{display:flex;flex-direction:column;justify-content:flex-end;align-items:flex-end;gap:6px;max-height:84px;overflow:hidden;-webkit-mask-image:linear-gradient(to bottom,transparent,#000 30%);mask-image:linear-gradient(to bottom,transparent,#000 30%)}
.ask-thread:empty{display:none}
.ask-msg{max-width:85%;padding:7px 11px;border-radius:14px 14px 4px 14px;background:linear-gradient(180deg,#2c2c31,#1d1d21);border:1px solid #34343a;color:var(--ask-ink);font-size:12px;animation:ask-in .35s cubic-bezier(.2,.8,.2,1);overflow-wrap:anywhere}
@keyframes ask-in{from{transform:translateY(8px) scale(.96);opacity:0}}
/* the frame: a 1.5px hairline that is bright at the top-left and fades into the card */
.ask-frame{position:relative;padding:1.5px;border-radius:18px;background:linear-gradient(135deg,#8a8a92 0%,#3a3a40 30%,#2a2a2f 100%);box-shadow:0 18px 40px -18px rgba(0,0,0,.8)}
.ask-frame::before{content:"";position:absolute;top:-12px;left:-12px;width:34px;height:34px;border-radius:50%;pointer-events:none;
background:radial-gradient(circle,rgba(255,255,255,.95) 0%,rgba(255,255,255,.35) 22%,rgba(255,255,255,.08) 45%,transparent 70%);filter:blur(1px)}
.ask-frame:focus-within{background:linear-gradient(135deg,#b9b9c2 0%,#4a4a52 35%,#2d2d33 100%)}
.ask-card{border-radius:16.5px;background:radial-gradient(120% 140% at 0% 0%,#1f1f23 0%,var(--ask-bg) 60%);overflow:hidden}
.ask-input{display:block;width:100%;min-height:58px;max-height:120px;padding:13px 14px 4px;border:0;background:transparent;color:var(--ask-ink);font:inherit;resize:none;outline:none;
scrollbar-width:thin;scrollbar-color:#555 transparent}
.ask-input::placeholder{color:#e9ecf3;opacity:.9;transition:opacity .25s}
.ask-input:focus::placeholder{opacity:.28}
.ask-bar{display:flex;align-items:flex-end;justify-content:space-between;padding:6px 10px 10px 8px}
.ask-tools{display:flex;gap:2px}
.ask-tool{position:relative;display:grid;place-items:center;width:30px;height:30px;padding:0;border:0;border-radius:9px;background:none;color:var(--ask-dim);cursor:pointer;transition:color .25s,transform .25s,background .25s}
.ask-tool svg{width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
.ask-tool:hover,.ask-tool:focus-visible{color:#fff;transform:translateY(-3px);outline:none}
.ask-tool:focus-visible{background:rgba(255,255,255,.08)}
.ask-tool[aria-pressed=true]{color:#8ab4ff}
.ask-tool[data-tip]::after{content:attr(data-tip);position:absolute;bottom:calc(100% + 6px);left:50%;transform:translateX(-50%) translateY(4px);padding:3px 7px;border-radius:6px;background:#2a2a2f;color:#fff;font-size:10.5px;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .2s,transform .2s}
.ask-tool:hover::after,.ask-tool:focus-visible::after{opacity:1;transform:translateX(-50%)}
.ask-right{display:flex;align-items:center;gap:8px}
.ask-count{color:var(--ask-dim);font-size:10.5px;font-variant-numeric:tabular-nums;opacity:0;transition:opacity .2s}
.ask-card:focus-within .ask-count{opacity:1}
/* metallic send button with a glossy top edge */
.ask-send{display:grid;place-items:center;padding:2px;border:0;border-radius:11px;cursor:pointer;background:linear-gradient(to top,#28282c,#58585f,#28282c);
box-shadow:inset 0 6px 2px -4px rgba(255,255,255,.5),0 6px 14px -6px rgba(0,0,0,.8);transition:transform .15s,filter .2s}
.ask-send span{display:grid;place-items:center;width:32px;height:32px;border-radius:9px;background:rgba(0,0,0,.12);-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);color:#8d8d94;overflow:hidden}
.ask-send svg{width:17px;height:17px;transition:transform .3s cubic-bezier(.3,1.4,.5,1),color .25s,filter .25s}
.ask-send:not(:disabled):hover svg,.ask-send:focus-visible svg{color:#f3f6fd;filter:drop-shadow(0 0 5px #fff);transform:rotate(45deg) translate(-1px,1px) scale(1.12)}
.ask-send:active:not(:disabled){transform:scale(.92)}
.ask-send:disabled{cursor:default;filter:grayscale(1) brightness(.75)}
.ask-send:focus-visible{outline:2px solid rgba(255,255,255,.7);outline-offset:2px}
.ask-send.is-fly svg{animation:ask-fly .55s cubic-bezier(.5,0,.75,0) forwards}
@keyframes ask-fly{0%{transform:rotate(45deg) scale(1.1)}35%{transform:rotate(45deg) translate(-4px,4px) scale(1)}100%{transform:rotate(45deg) translate(40px,-40px) scale(.6);opacity:0}}
.ask-chips{display:flex;flex-wrap:wrap;gap:6px}
.ask-chip{height:28px;padding:0 11px;border:1.5px solid #34343a;border-radius:10px;background:#18181b;color:#e9ecf3;font:500 11.5px system-ui,sans-serif;cursor:pointer;transition:background .2s,border-color .2s,transform .2s}
.ask-chip:hover,.ask-chip:focus-visible{background:#26262b;border-color:#55555d;transform:translateY(-2px);outline:none}
.ask-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}
@media (prefers-reduced-motion:reduce){.ask-msg,.ask-send.is-fly svg{animation:none}.ask-tool:hover,.ask-chip:hover{transform:none}}`;
const Icon = ({ d }: { d: string }) => <svg viewBox="0 0 24 24" dangerouslySetInnerHTML={{ __html: d }} />;
const ICONS = { attach: `<path d="M20.5 11.5 12.4 19.6a5 5 0 0 1-7.1-7.1l8.5-8.5a3.3 3.3 0 0 1 4.7 4.7l-8.5 8.5a1.7 1.7 0 0 1-2.4-2.4l7.8-7.8"/>`, image: `<rect x="3.5" y="4.5" width="17" height="15" rx="3"/><circle cx="9" cy="10" r="1.8"/><path d="m20.5 16-4.6-4.6L6 19.5"/>`, web: `<circle cx="12" cy="12" r="8.5"/><path d="M3.5 12h17M12 3.5c2.4 2.3 2.4 14.7 0 17M12 3.5c-2.4 2.3-2.4 14.7 0 17"/>` };
export type PromptTool = "attach" | "image" | "web";
export function AIPromptBox({
onSubmit,
onTool,
placeholder = "Imagine something... ✦",
suggestions = ["Create an image", "Analyse data", "Write a plan"],
maxLength = 2000,
showThread = true,
disabled,
}: {
/** Called with the prompt and the toggled tools (e.g. ["web"]). */
onSubmit: (text: string, tools: PromptTool[]) => void;
/** Attach and image buttons call this so you can open your own picker. */
onTool?: (tool: PromptTool) => void;
placeholder?: string;
suggestions?: string[];
maxLength?: number;
showThread?: boolean;
disabled?: boolean;
}) {
const [text, setText] = useState("");
const [web, setWeb] = useState(false);
const [sent, setSent] = useState<{ id: number; text: string }[]>([]);
const [fly, setFly] = useState(0);
const input = useRef<HTMLTextAreaElement>(null);
const sendBtn = useRef<HTMLButtonElement>(null);
useEffect(() => {
const el = input.current;
if (!el) return;
el.style.height = "auto";
el.style.height = `${Math.min(el.scrollHeight, 120)}px`;
}, [text]);
useEffect(() => {
const b = sendBtn.current;
if (!fly || !b) return;
b.classList.remove("is-fly"); void b.offsetWidth; b.classList.add("is-fly");
const t = setTimeout(() => b.classList.remove("is-fly"), 600);
return () => clearTimeout(t);
}, [fly]);
const submit = () => {
const value = text.trim();
if (!value || disabled) return;
onSubmit(value, web ? ["web"] : []);
setSent((s) => [...s, { id: Date.now(), text: value }].slice(-3));
setText("");
setFly((n) => n + 1);
input.current?.focus();
};
return (
<form className="ask" onSubmit={(e) => { e.preventDefault(); submit(); }} noValidate>
<style>{css}</style>
{showThread && (
<div className="ask-thread" aria-live="polite" aria-label="Sent messages">
{sent.map((m) => <div key={m.id} className="ask-msg">{m.text}</div>)}
</div>
)}
<div className="ask-frame"><div className="ask-card">
<textarea
ref={input} className="ask-input" rows={2} maxLength={maxLength} placeholder={placeholder} aria-label="Message"
value={text} disabled={disabled} onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault(); submit(); } }}
/>
<div className="ask-bar">
<div className="ask-tools">
<button type="button" className="ask-tool" data-tip="Attach a file" aria-label="Attach a file" onClick={() => onTool?.("attach")}><Icon d={ICONS.attach} /></button>
<button type="button" className="ask-tool" data-tip="Add an image" aria-label="Add an image" onClick={() => onTool?.("image")}><Icon d={ICONS.image} /></button>
<button type="button" className="ask-tool" data-tip="Search the web" aria-label="Search the web" aria-pressed={web} onClick={() => setWeb((w) => !w)}><Icon d={ICONS.web} /></button>
</div>
<div className="ask-right">
<span className="ask-count">{text.length}/{maxLength}</span>
<button ref={sendBtn} type="button" className="ask-send" aria-label="Send" disabled={!text.trim() || disabled} onClick={submit}>
<span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 3 10.5 13.5M21 3l-6.5 18-4-7.5L3 9.5z" /></svg></span>
</button>
</div>
</div>
</div></div>
{suggestions.length > 0 && (
<div className="ask-chips" aria-label="Suggestions">
{suggestions.map((s) => (
<button key={s} type="button" className="ask-chip" onClick={() => { setText(`${s}: `); input.current?.focus(); }}>{s}</button>
))}
</div>
)}
</form>
);
}
/* Usage:
<AIPromptBox onSubmit={(text, tools) => sendToAI({ text, webSearch: tools.includes("web") })} onTool={(t) => t === "attach" && openFilePicker()} />
*/