feat(filelist): added file list ui
Drop down menu that shows it all
This commit is contained in:
parent
db99d625c4
commit
8917a4d1a5
5 changed files with 127 additions and 87 deletions
|
|
@ -6,7 +6,7 @@ export default function DownButton({ onClick }) {
|
|||
return (
|
||||
<motion.button
|
||||
onClick={onClick}
|
||||
className="bg-gray-700 p-2 rounded-2xl file-input border-2 border-gray-600"
|
||||
className="bg-gray-700 p-2 rounded-2xl file-input border-2 border-gray-600 size-10"
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,32 +1,22 @@
|
|||
import React, { useState, useRef } from "react";
|
||||
import { X } from "lucide-react";
|
||||
import React, { forwardRef, useRef } from "react";
|
||||
import { motion } from "motion/react";
|
||||
import { FilePlus2 } from "lucide-react";
|
||||
|
||||
export default function SchematicButton({ onFiles }) {
|
||||
const [filesList, setFilesList] = useState([]);
|
||||
// Hidden file input that exposes an open() method via ref
|
||||
const SchematicButton = forwardRef(function SchematicButton({ onFiles }, ref) {
|
||||
const inputRef = useRef(null);
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
open: () => inputRef.current && inputRef.current.click(),
|
||||
}));
|
||||
|
||||
function handleFiles(e) {
|
||||
const files = Array.from(e.target.files || []);
|
||||
if (files.length === 0) return;
|
||||
|
||||
setFilesList((s) => [...s, ...files]);
|
||||
if (onFiles) onFiles(files);
|
||||
if (inputRef.current) inputRef.current.value = null;
|
||||
}
|
||||
|
||||
function removeFile(index) {
|
||||
setFilesList((s) => {
|
||||
const copy = [...s];
|
||||
copy.splice(index, 1);
|
||||
return copy;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="relative inline-block">
|
||||
<motion.input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
|
|
@ -37,35 +27,7 @@ export default function SchematicButton({ onFiles }) {
|
|||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
/>
|
||||
<motion.div
|
||||
className="bg-gray-700 p-2 rounded-2xl cursor-pointer border-2 border-gray-600"
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={() => inputRef.current && inputRef.current.click()}
|
||||
>
|
||||
<FilePlus2 />
|
||||
</motion.div>
|
||||
</label>
|
||||
|
||||
{filesList.length > 0 && (
|
||||
<div className="flex gap-2 items-center max-w-xs flex-wrap">
|
||||
{filesList.map((f, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center gap-2 bg-gray-800 text-sm text-slate-200 px-2 py-1 rounded"
|
||||
>
|
||||
<span className="truncate max-w-[10rem]">{f.name}</span>
|
||||
<button
|
||||
onClick={() => removeFile(i)}
|
||||
className="bg-gray-900 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs border-2 border-gray-700"
|
||||
aria-label="Remove file"
|
||||
>
|
||||
<X />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default SchematicButton;
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import React, { useMemo, useState } from "react";
|
|||
import { motion } from "motion/react";
|
||||
import { Rocket } from "lucide-react";
|
||||
import DeleteButton from "src/components/ui/button/delete-button";
|
||||
import SchematicButton from "../button/schematic-button";
|
||||
import FileList from "src/components/ui/file/file-list";
|
||||
|
||||
export default function ChatHeader({ title = "Title of Chat" }) {
|
||||
export default function ChatHeader({ title = "Schematic Spelunker" }) {
|
||||
const isDebug = useMemo(() => {
|
||||
const p = new URLSearchParams(window.location.search);
|
||||
return p.get("debug") === "1";
|
||||
|
|
@ -17,7 +17,9 @@ export default function ChatHeader({ title = "Title of Chat" }) {
|
|||
setIngesting(true);
|
||||
const res = await fetch("/api/files/import-demo", { method: "POST" });
|
||||
const json = await res.json().catch(() => ({}));
|
||||
setToast(`Imported: ${json.imported ?? "?"}, Skipped: ${json.skipped ?? "?"}`);
|
||||
setToast(
|
||||
`Imported: ${json.imported ?? "?"}, Skipped: ${json.skipped ?? "?"}`
|
||||
);
|
||||
setTimeout(() => setToast(""), 4000);
|
||||
} catch (e) {
|
||||
setToast("Import failed");
|
||||
|
|
@ -31,9 +33,8 @@ export default function ChatHeader({ title = "Title of Chat" }) {
|
|||
<div className="w-full flex justify-center">
|
||||
<header className="text-slate-100 fixed top-4 max-w-3xl w-full px-4">
|
||||
<div className="flex justify-between items-center gap-4">
|
||||
<SchematicButton />
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-lg font-semibold shadow-md shadow-indigo-600 bg-gray-900 px-6 py-2 rounded-4xl border-2 border-gray-800">
|
||||
<FileList />
|
||||
<h1 className="text-sm lg:text-lg font-semibold shadow-md shadow-indigo-600 bg-gray-900 px-6 py-2 rounded-4xl border-2 border-gray-800">
|
||||
{title}
|
||||
</h1>
|
||||
<DeleteButton />
|
||||
|
|
@ -50,7 +51,6 @@ export default function ChatHeader({ title = "Title of Chat" }) {
|
|||
</motion.button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{toast && (
|
||||
<div className="mt-2 text-xs text-slate-300 bg-gray-800/80 border border-gray-700 rounded px-2 py-1 inline-block">
|
||||
{toast}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import React, { useState, useRef, useEffect } from "react";
|
||||
import DeleteButton from "src/components/ui/button/delete-button";
|
||||
import DownButton from "src/components/ui/button/down-button";
|
||||
import { motion } from "motion/react";
|
||||
import { BotMessageSquare } from "lucide-react";
|
||||
|
|
@ -24,11 +23,7 @@ export default function MessageInput({ onSend }) {
|
|||
<div className="w-full flex justify-center">
|
||||
<footer className="fixed bottom-6 max-w-3xl w-full px-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex justify-between items-end">
|
||||
<div className="flex">
|
||||
<DownButton></DownButton>
|
||||
</div>
|
||||
</div>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="bg-gray-900 rounded-2xl border-2 border-gray-800 shadow-lg shadow-indigo-600"
|
||||
|
|
|
|||
83
web-app/src/components/ui/file/file-list.jsx
Normal file
83
web-app/src/components/ui/file/file-list.jsx
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import React, { useRef, useState } from "react";
|
||||
import SchematicButton from "src/components/ui/button/schematic-button";
|
||||
import { motion } from "motion/react";
|
||||
import { Menu } from "lucide-react";
|
||||
import { X } from "lucide-react";
|
||||
import { FilePlus2 } from "lucide-react";
|
||||
|
||||
export default function FileList() {
|
||||
const pickerRef = useRef(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [files, setFiles] = useState([]);
|
||||
|
||||
function handleAdd() {
|
||||
if (pickerRef.current && pickerRef.current.open) pickerRef.current.open();
|
||||
}
|
||||
|
||||
function handleFiles(selected) {
|
||||
setFiles((s) => [...s, ...selected]);
|
||||
setOpen(true);
|
||||
}
|
||||
|
||||
function removeFile(i) {
|
||||
setFiles((s) => s.filter((_, idx) => idx !== i));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<motion.button
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="p-2 rounded-xl bg-gray-700 border-2 border-gray-600"
|
||||
aria-expanded={open}
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTab={{ scale: 0.9 }}
|
||||
>
|
||||
{open ? <X /> : <Menu />}
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
{open && (
|
||||
<div className="fixed left-1/2 top-24 transform -translate-x-1/2 z-50 w-full max-w-3xl px-4">
|
||||
<div className="bg-gray-900 border-2 border-gray-800 rounded-2xl p-4 shadow-lg overflow-auto">
|
||||
<div className="flex items-center justify-between mb-2 pr-1">
|
||||
<div className="text-lg font-medium">Files</div>
|
||||
<div>
|
||||
<motion.button
|
||||
onClick={handleAdd}
|
||||
className="w-full bg-gray-700 text-sm p-2 rounded-full border-2 border-gray-600"
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
>
|
||||
<FilePlus2 />
|
||||
</motion.button>
|
||||
<SchematicButton ref={pickerRef} onFiles={handleFiles} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
{files.length === 0 ? (
|
||||
<div className="text-md text-slate-400">No files added</div>
|
||||
) : (
|
||||
files.map((f, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center justify-between bg-gray-800 p-2 rounded-lg text-sm"
|
||||
>
|
||||
<span className="truncate max-w-[24rem]">{f.name}</span>
|
||||
<button
|
||||
onClick={() => removeFile(i)}
|
||||
className="text-xs bg-gray-700 rounded-full p-2"
|
||||
>
|
||||
<X />
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue