/ frontend / src / app / views / embeddings / List.jsx
List.jsx
  1  import { useState, useEffect } from "react";
  2  import { Box, Button, Chip, IconButton, Tooltip, styled } from "@mui/material";
  3  import { Add, Edit, Delete, Visibility, Hub } from "@mui/icons-material";
  4  import { useNavigate } from "react-router-dom";
  5  import { toast } from "react-toastify";
  6  import useAuth from "app/hooks/useAuth";
  7  import Breadcrumb from "app/components/Breadcrumb";
  8  import { useTranslation } from "react-i18next";
  9  import DataList from "app/components/DataList";
 10  import api from "app/utils/api";
 11  
 12  const Container = styled("div")(({ theme }) => ({
 13    margin: "24px 48px",
 14    [theme.breakpoints.down("md")]: { margin: "24px 32px" },
 15    [theme.breakpoints.down("sm")]: { margin: 16 },
 16    "& .breadcrumb": { marginBottom: 24 },
 17  }));
 18  
 19  export default function Embeddings() {
 20    const { t } = useTranslation();
 21    const [embeddings, setEmbeddings] = useState([]);
 22    const auth = useAuth();
 23    const navigate = useNavigate();
 24    const isAdmin = auth.user?.is_admin;
 25  
 26    const fetchEmbeddings = () => {
 27      api.get("/embeddings", auth.user.token)
 28        .then((d) => setEmbeddings(Array.isArray(d) ? d : (d?.embeddings || [])))
 29        .catch(() => {});
 30    };
 31  
 32    useEffect(() => {
 33      document.title = (process.env.REACT_APP_RESTAI_NAME || "RESTai") + " - Embeddings";
 34      fetchEmbeddings();
 35    }, []);
 36  
 37    const handleDelete = (e, em) => {
 38      e.stopPropagation();
 39      if (!window.confirm(t("embeddings.info.deleteConfirm", { name: em.name }))) return;
 40      api.delete("/embeddings/" + em.id, auth.user.token)
 41        .then(() => {
 42          toast.success(t("embeddings.info.deleted", { name: em.name }));
 43          fetchEmbeddings();
 44        })
 45        .catch(() => {});
 46    };
 47  
 48    const embTooltip = (row) => (
 49      <Box sx={{ p: 0.5, maxWidth: 320 }}>
 50        <Box sx={{ fontWeight: 600, mb: 0.5 }}>{row.name}</Box>
 51        {row.description && (
 52          <Box sx={{ mb: 0.75, fontSize: "0.8rem", color: "grey.100", lineHeight: 1.4 }}>
 53            {row.description}
 54          </Box>
 55        )}
 56        <Box sx={{ fontSize: "0.75rem", lineHeight: 1.5 }}>
 57          <div><strong>{t("embeddings.tooltip.class")}:</strong> {row.class_name}</div>
 58          <div><strong>{t("embeddings.tooltip.privacy")}:</strong> {row.privacy}</div>
 59          {row.dimension && (
 60            <div><strong>{t("embeddings.tooltip.dimensions")}:</strong> {row.dimension}</div>
 61          )}
 62        </Box>
 63      </Box>
 64    );
 65  
 66    const columns = [
 67      {
 68        key: "name",
 69        label: t("embeddings.columns.name"),
 70        sortable: true,
 71        render: (row) => (
 72          <Tooltip title={embTooltip(row)} placement="right" arrow>
 73            <Box sx={{ display: "flex", alignItems: "center", gap: 1.5, cursor: "help" }}>
 74              <Box
 75                sx={{
 76                  width: 32, height: 32, borderRadius: "8px",
 77                  display: "flex", alignItems: "center", justifyContent: "center",
 78                  background: (t) => t.palette.mode === "dark" ? "rgba(249,115,22,0.15)" : "rgba(249,115,22,0.1)",
 79                }}
 80              >
 81                <Hub sx={{ fontSize: 18, color: "#f97316" }} />
 82              </Box>
 83              <Box sx={{ fontWeight: 500 }}>{row.name}</Box>
 84            </Box>
 85          </Tooltip>
 86        ),
 87      },
 88      {
 89        key: "class_name",
 90        label: t("embeddings.columns.class"),
 91        sortable: true,
 92        render: (row) => (
 93          <Box sx={{ fontFamily: "monospace", fontSize: "0.85rem", color: "text.secondary" }}>
 94            {row.class_name}
 95          </Box>
 96        ),
 97      },
 98      {
 99        key: "privacy",
100        label: t("embeddings.columns.privacy"),
101        sortable: true,
102        render: (row) => (
103          <Chip
104            label={row.privacy}
105            size="small"
106            sx={{
107              backgroundColor: row.privacy === "private" ? "rgba(239,68,68,0.12)" : "rgba(16,185,129,0.12)",
108              color: row.privacy === "private" ? "#ef4444" : "#10b981",
109              fontWeight: 600,
110              fontSize: "0.72rem",
111              textTransform: "uppercase",
112              height: 22,
113            }}
114          />
115        ),
116      },
117      {
118        key: "dimension",
119        label: t("embeddings.columns.dimensions"),
120        sortable: true,
121        align: "right",
122        render: (row) => row.dimension ?? "—",
123      },
124    ];
125  
126    return (
127      <Container>
128        <Box className="breadcrumb">
129          <Breadcrumb routeSegments={[{ name: t("nav.embeddings"), path: "/embeddings" }]} />
130        </Box>
131  
132        <DataList
133          title={t("embeddings.title")}
134          subtitle={t("embeddings.subtitle")}
135          data={embeddings}
136          columns={columns}
137          searchKeys={["name", "class_name"]}
138          filters={[
139            {
140              key: "privacy",
141              label: t("embeddings.columns.privacy"),
142              options: [
143                { value: "private", label: t("common.private") },
144                { value: "public", label: t("common.public") },
145              ],
146            },
147          ]}
148          onRowClick={(row) => navigate(`/embedding/${row.id}`)}
149          rowKey={(row) => row.id}
150          defaultSort={{ key: "name", direction: "asc" }}
151          headerAction={
152            isAdmin && (
153              <Button variant="contained" startIcon={<Add />} onClick={() => navigate("/embeddings/new")}>
154                {t("embeddings.newBreadcrumb")}
155              </Button>
156            )
157          }
158          actions={(row) => (
159            <>
160              <Tooltip title={t("embeddings.actions.view")}>
161                <IconButton size="small" onClick={() => navigate(`/embedding/${row.id}`)}>
162                  <Visibility fontSize="small" />
163                </IconButton>
164              </Tooltip>
165              {isAdmin && (
166                <>
167                  <Tooltip title={t("embeddings.actions.edit")}>
168                    <IconButton size="small" onClick={() => navigate(`/embedding/${row.id}/edit`)}>
169                      <Edit fontSize="small" />
170                    </IconButton>
171                  </Tooltip>
172                  <Tooltip title={t("embeddings.actions.delete")}>
173                    <IconButton size="small" color="error" onClick={(e) => handleDelete(e, row)}>
174                      <Delete fontSize="small" />
175                    </IconButton>
176                  </Tooltip>
177                </>
178              )}
179            </>
180          )}
181          emptyState={{
182            icon: Hub,
183            title: t("embeddings.emptyTitle"),
184            message: t("embeddings.emptyMessage"),
185            actionLabel: isAdmin ? t("embeddings.new") : undefined,
186            actionIcon: <Add fontSize="small" />,
187            onAction: isAdmin ? () => navigate("/embeddings/new") : undefined,
188          }}
189          emptyMessage={t("embeddings.noEmbeddings")}
190        />
191      </Container>
192    );
193  }