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 (
|
return (
|
||||||
<motion.button
|
<motion.button
|
||||||
onClick={onClick}
|
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 }}
|
whileHover={{ scale: 1.1 }}
|
||||||
whileTap={{ scale: 0.9 }}
|
whileTap={{ scale: 0.9 }}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,71 +1,33 @@
|
||||||
import React, { useState, useRef } from "react";
|
import React, { forwardRef, useRef } from "react";
|
||||||
import { X } from "lucide-react";
|
|
||||||
import { motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
import { FilePlus2 } from "lucide-react";
|
|
||||||
|
|
||||||
export default function SchematicButton({ onFiles }) {
|
// Hidden file input that exposes an open() method via ref
|
||||||
const [filesList, setFilesList] = useState([]);
|
const SchematicButton = forwardRef(function SchematicButton({ onFiles }, ref) {
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
|
React.useImperativeHandle(ref, () => ({
|
||||||
|
open: () => inputRef.current && inputRef.current.click(),
|
||||||
|
}));
|
||||||
|
|
||||||
function handleFiles(e) {
|
function handleFiles(e) {
|
||||||
const files = Array.from(e.target.files || []);
|
const files = Array.from(e.target.files || []);
|
||||||
if (files.length === 0) return;
|
if (files.length === 0) return;
|
||||||
|
|
||||||
setFilesList((s) => [...s, ...files]);
|
|
||||||
if (onFiles) onFiles(files);
|
if (onFiles) onFiles(files);
|
||||||
if (inputRef.current) inputRef.current.value = null;
|
if (inputRef.current) inputRef.current.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFile(index) {
|
|
||||||
setFilesList((s) => {
|
|
||||||
const copy = [...s];
|
|
||||||
copy.splice(index, 1);
|
|
||||||
return copy;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<motion.input
|
||||||
<label className="relative inline-block">
|
ref={inputRef}
|
||||||
<motion.input
|
type="file"
|
||||||
ref={inputRef}
|
accept="image/*,application/pdf"
|
||||||
type="file"
|
multiple
|
||||||
accept="image/*,application/pdf"
|
onChange={handleFiles}
|
||||||
multiple
|
className="file-input hidden"
|
||||||
onChange={handleFiles}
|
whileHover={{ scale: 1.02 }}
|
||||||
className="file-input hidden"
|
whileTap={{ scale: 0.98 }}
|
||||||
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 { motion } from "motion/react";
|
||||||
import { Rocket } from "lucide-react";
|
import { Rocket } from "lucide-react";
|
||||||
import DeleteButton from "src/components/ui/button/delete-button";
|
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 isDebug = useMemo(() => {
|
||||||
const p = new URLSearchParams(window.location.search);
|
const p = new URLSearchParams(window.location.search);
|
||||||
return p.get("debug") === "1";
|
return p.get("debug") === "1";
|
||||||
|
|
@ -17,7 +17,9 @@ export default function ChatHeader({ title = "Title of Chat" }) {
|
||||||
setIngesting(true);
|
setIngesting(true);
|
||||||
const res = await fetch("/api/files/import-demo", { method: "POST" });
|
const res = await fetch("/api/files/import-demo", { method: "POST" });
|
||||||
const json = await res.json().catch(() => ({}));
|
const json = await res.json().catch(() => ({}));
|
||||||
setToast(`Imported: ${json.imported ?? "?"}, Skipped: ${json.skipped ?? "?"}`);
|
setToast(
|
||||||
|
`Imported: ${json.imported ?? "?"}, Skipped: ${json.skipped ?? "?"}`
|
||||||
|
);
|
||||||
setTimeout(() => setToast(""), 4000);
|
setTimeout(() => setToast(""), 4000);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setToast("Import failed");
|
setToast("Import failed");
|
||||||
|
|
@ -31,25 +33,23 @@ export default function ChatHeader({ title = "Title of Chat" }) {
|
||||||
<div className="w-full flex justify-center">
|
<div className="w-full flex justify-center">
|
||||||
<header className="text-slate-100 fixed top-4 max-w-3xl w-full px-4">
|
<header className="text-slate-100 fixed top-4 max-w-3xl w-full px-4">
|
||||||
<div className="flex justify-between items-center gap-4">
|
<div className="flex justify-between items-center gap-4">
|
||||||
<SchematicButton />
|
<FileList />
|
||||||
<div className="flex items-center gap-3">
|
<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">
|
||||||
<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">
|
{title}
|
||||||
{title}
|
</h1>
|
||||||
</h1>
|
<DeleteButton />
|
||||||
<DeleteButton />
|
{isDebug && (
|
||||||
{isDebug && (
|
<motion.button
|
||||||
<motion.button
|
onClick={triggerDemoIngest}
|
||||||
onClick={triggerDemoIngest}
|
className="bg-gray-800 border-2 border-gray-700 rounded-xl px-3 py-2 flex items-center gap-2"
|
||||||
className="bg-gray-800 border-2 border-gray-700 rounded-xl px-3 py-2 flex items-center gap-2"
|
whileHover={{ scale: 1.05 }}
|
||||||
whileHover={{ scale: 1.05 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
disabled={ingesting}
|
||||||
disabled={ingesting}
|
>
|
||||||
>
|
<Rocket size={16} />
|
||||||
<Rocket size={16} />
|
{ingesting ? "Seeding…" : "Seed Demo Data"}
|
||||||
{ingesting ? "Seeding…" : "Seed Demo Data"}
|
</motion.button>
|
||||||
</motion.button>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{toast && (
|
{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">
|
<div className="mt-2 text-xs text-slate-300 bg-gray-800/80 border border-gray-700 rounded px-2 py-1 inline-block">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import React, { useState, useRef, useEffect } from "react";
|
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 DownButton from "src/components/ui/button/down-button";
|
||||||
import { motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
import { BotMessageSquare } from "lucide-react";
|
import { BotMessageSquare } from "lucide-react";
|
||||||
|
|
@ -24,11 +23,7 @@ export default function MessageInput({ onSend }) {
|
||||||
<div className="w-full flex justify-center">
|
<div className="w-full flex justify-center">
|
||||||
<footer className="fixed bottom-6 max-w-3xl w-full px-4">
|
<footer className="fixed bottom-6 max-w-3xl w-full px-4">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex justify-between items-end">
|
<DownButton></DownButton>
|
||||||
<div className="flex">
|
|
||||||
<DownButton></DownButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
className="bg-gray-900 rounded-2xl border-2 border-gray-800 shadow-lg shadow-indigo-600"
|
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