ResultsPanel

작성자

카테고리:

← 피드로
DEV Community · Rahul · 2026-08-12 개발(SW)

Rahul

Rahul

Posted on Aug 12 • Edited on Aug 13

!–

Disable liquid parsing on this codeblock to prevent errors reading ‘{{‘
See: https://talk.jekyllrb.com/t/code-block-is-improperly-handled-and-generates-liquid-syntax-error/7599/2
–>
{% raw %}

import {
  AlertTriangle,
  ArrowDownToLine,
  CheckCircle2,
  ChevronRight,
  Code2,
  Database,
  GitBranch,
  ListTree,
  ShieldCheck,
  Sparkles,
  ThumbsDown,
  ThumbsUp,
} from "lucide-react";
import { useMemo, useState } from "react";
import { api, field, type ApiItem, type JsonObject } from "../lib/api";

type ResultTab =
  | "overview"
  | "flow"
  | "procedures"
  | "data"
  | "dependencies"
  | "evidence"
  | "raw";

const tabs: Array<{ id: ResultTab; label: string }> = [
  { id: "overview", label: "Overview" },
  { id: "flow", label: "Execution flow" },
  { id: "procedures", label: "Subprograms" },
  { id: "data", label: "Data operations" },
  { id: "dependencies", label: "Dependencies" },
  { id: "evidence", label: "Evidence" },
  { id: "raw", label: "Raw metadata" },
];

function asObject(value: unknown): ApiItem {
  return value && typeof value === "object" && !Array.isArray(value)
    ? (value as ApiItem)
    : {};
}

function asArray(value: unknown): ApiItem[] {
  return Array.isArray(value) ? value.map(asObject) : [];
}

function asStrings(value: unknown): string[] {
  return Array.isArray(value) ? value.map(String) : [];
}

function download(filename: string, content: string, mime: string) {
  const blob = new Blob([content], { type: mime });
  const url = URL.createObjectURL(blob);
  const anchor = document.createElement("a");
  anchor.href = url;
  anchor.download = filename;
  document.body.appendChild(anchor);
  anchor.click();
  anchor.remove();
  URL.revokeObjectURL(url);
}

function getFinalResult(run: ApiItem | null): JsonObject | null {
  if (!run) return null;
  const direct = run.result ?? run.RESULT;
  if (direct && typeof direct === "object" && !Array.isArray(direct)) {
    return direct as JsonObject;
  }
  const results = (run.results ?? run.RESULTS) as unknown;
  if (!Array.isArray(results)) return null;
  for (let index = results.length - 1; index >= 0; index -= 1) {
    const row = asObject(results[index]);
    if (field(row, "RESULT_TYPE", "") === "FINAL") {
      const result = row.RESULT ?? row.result;
      if (result && typeof result === "object" && !Array.isArray(result)) {
        return result as JsonObject;
      }
    }
  }
  return null;
}

export function ResultsPanel({
  run,
  runId,
  isAdmin,
}: {
  run: ApiItem | null;
  runId: string | null;
  isAdmin: boolean;
}) {
  const [activeTab, setActiveTab] = useState<ResultTab>("overview");
  const [feedbackState, setFeedbackState] = useState<
    "idle" | "sending" | "sent"
  >("idle");
  const result = useMemo(() => getFinalResult(run), [run]);

  if (!result) return null;

  const source = asObject(result.source);
  const metadata = asObject(result.metadata);
  const packageResult = asObject(result.package);
  const procedures = asArray(result.procedures);
  const objectName = field(
    metadata,
    "object_name",
    field(packageResult, "package_name", "PL/SQL object"),
  );
  const sourceLines = Number(
    field(source, "total_lines", field(metadata, "total_lines", 0)),
  );
  const confidence = Number(field(packageResult, "confidence", 0));

  const sendFeedback = async (rating: "UP" | "DOWN") => {
    if (!runId || feedbackState !== "idle") return;
    setFeedbackState("sending");
    try {
      await api.feedback(runId, rating);
      setFeedbackState("sent");
    } catch {
      setFeedbackState("idle");
    }
  };

  const exportJson = () =>
    download(
      `${objectName || "analysis"}.json`,
      JSON.stringify(result, null, 2),
      "application/json",
    );

  return (
    <section className="results-shell" aria-label="Analysis results">
      <div className="results-heading">
        <div>
          <span className="eyebrow">
            <Sparkles size={13} /> Analysis complete
          </span>
          <h2>{objectName}</h2>
          <p>
            Evidence-grounded analysis generated from the current source and
            deterministic metadata.
          </p>
        </div>
        <button className="button secondary" onClick={exportJson}>
          <ArrowDownToLine size={16} />
          Export JSON
        </button>
      </div>

      <div className="metric-grid result-metrics">
        <article className="metric-card">
          <span>Object type</span>
          <strong>{field(metadata, "object_type", "Unknown")}</strong>
          <Code2 size={18} />
        </article>
        <article className="metric-card">
          <span>Source lines</span>
          <strong>{sourceLines.toLocaleString()}</strong>
          <ListTree size={18} />
        </article>
        <article className="metric-card">
          <span>Subprograms</span>
          <strong>{procedures.length}</strong>
          <GitBranch size={18} />
        </article>
        <article className="metric-card">
          <span>Confidence</span>
          <strong>{Math.round(confidence * 100)}%</strong>
          <ShieldCheck size={18} />
        </article>
      </div>

      <div className="tab-strip" role="tablist" aria-label="Result sections">
        {tabs.map((tab) => (
          <button
            key={tab.id}
            role="tab"
            aria-selected={activeTab === tab.id}
            className={activeTab === tab.id ? "active" : ""}
            onClick={() => setActiveTab(tab.id)}
          >
            {tab.label}
          </button>
        ))}
      </div>

      <div className="result-content">
        {activeTab === "overview" && (
          <Overview result={result} packageResult={packageResult} />
        )}
        {activeTab === "flow" && <Flow packageResult={packageResult} />}
        {activeTab === "procedures" && (
          <Procedures procedures={procedures} />
        )}
        {activeTab === "data" && (
          <DataOperations metadata={metadata} procedures={procedures} />
        )}
        {activeTab === "dependencies" && (
          <Dependencies packageResult={packageResult} />
        )}
        {activeTab === "evidence" && (
          <Evidence result={result} source={source} />
        )}
        {activeTab === "raw" && (
          <pre className="json-view">
            <code>{JSON.stringify(metadata, null, 2)}</code>
          </pre>
        )}
      </div>

      {isAdmin ? (
        <footer className="result-footer">
          <div>
            <strong>Was this analysis useful?</strong>
            <span>
              Approved feedback improves future retrieval. Negative feedback is
              never promoted.
            </span>
          </div>
          <div className="feedback-actions">
            <button
              className="icon-text-button"
              disabled={!runId || feedbackState !== "idle"}
              onClick={() => void sendFeedback("UP")}
            >
              <ThumbsUp size={16} /> Useful
            </button>
            <button
              className="icon-text-button"
              disabled={!runId || feedbackState !== "idle"}
              onClick={() => void sendFeedback("DOWN")}
            >
              <ThumbsDown size={16} /> Needs work
            </button>
            {feedbackState === "sent" && (
              <span className="feedback-sent">
                <CheckCircle2 size={15} /> Recorded
              </span>
            )}
          </div>
        </footer>
      ) : (
        <footer className="result-footer transient-result-footer">
          <ShieldCheck size={20} />
          <div>
            <strong>Private, transient result</strong>
            <span>
              This analysis was returned to you without changing shared
              database knowledge.
            </span>
          </div>
        </footer>
      )}
    </section>
  );
}

function Overview({
  result,
  packageResult,
}: {
  result: JsonObject;
  packageResult: ApiItem;
}) {
  const warnings = [
    ...asStrings(result.warnings),
    ...asStrings(packageResult.risks),
    ...asStrings(packageResult.unknowns),
  ];
  const entries = asStrings(packageResult.entry_points);
  return (
    <div className="result-two-column">
      <article className="content-card span-two">
        <div className="content-card-title">
          <Sparkles size={17} />
          <h3>Purpose and behavior</h3>
        </div>
        <p className="lead-copy">
          {field(
            packageResult,
            "purpose",
            field(packageResult, "overview", "No package overview is available."),
          )}
        </p>
        {field(packageResult, "functional_summary", "") && (
          <>
            <h4>Functional summary</h4>
            <p>{field(packageResult, "functional_summary", "")}</p>
          </>
        )}
      </article>
      <article className="content-card">
        <div className="content-card-title">
          <GitBranch size={17} />
          <h3>Entry points</h3>
        </div>
        {entries.length ? (
          <ul className="clean-list">
            {entries.map((entry) => (
              <li key={entry}>
                <ChevronRight size={14} />
                <code>{entry}</code>
              </li>
            ))}
          </ul>
        ) : (
          <p className="empty-copy">No public entry points were reported.</p>
        )}
      </article>
      <article className="content-card">
        <div className="content-card-title">
          <AlertTriangle size={17} />
          <h3>Warnings and uncertainty</h3>
        </div>
        {warnings.length ? (
          <ul className="warning-list">
            {warnings.map((warning, index) => (
              <li key={`${warning}-${index}`}>{warning}</li>
            ))}
          </ul>
        ) : (
          <div className="positive-state">
            <CheckCircle2 size={18} />
            No material warnings were reported.
          </div>
        )}
      </article>
    </div>
  );
}

function Flow({ packageResult }: { packageResult: ApiItem }) {
  const workflow = asStrings(packageResult.workflow);
  return workflow.length ? (
    <ol className="execution-flow">
      {workflow.map((step, index) => (
        <li key={`${step}-${index}`}>
          <span>{String(index + 1).padStart(2, "0")}</span>
          <div>
            <strong>{step}</strong>
          </div>
        </li>
      ))}
    </ol>
  ) : (
    <EmptyResult text="No merged execution workflow was returned." />
  );
}

function Procedures({ procedures }: { procedures: ApiItem[] }) {
  if (!procedures.length) {
    return <EmptyResult text="No procedures or functions were returned." />;
  }
  return (
    <div className="procedure-list">
      {procedures.map((procedure, index) => {
        const lineRange = Array.isArray(procedure.line_range)
          ? procedure.line_range
          : [0, 0];
        const audit = field(procedure, "audit_status", "UNKNOWN");
        const confidence = Number(field(procedure, "confidence", 0));
        return (
          <details className="procedure-card" key={`${field(procedure, "subprogram_name", "item")}-${index}`}>
            <summary>
              <div className="procedure-identity">
                <span className="procedure-icon">
                  <Code2 size={17} />
                </span>
                <div>
                  <strong>
                    {field(procedure, "subprogram_name", `Subprogram ${index + 1}`)}
                  </strong>
                  <span>
                    {field(procedure, "subprogram_type", "PL/SQL")} · lines{" "}
                    {String(lineRange[0])}–{String(lineRange[1])}
                  </span>
                </div>
              </div>
              <div className="procedure-badges">
                <span className={`status-chip ${audit === "PASS" ? "good" : "warn"}`}>
                  {audit}
                </span>
                <span className="status-chip neutral">
                  {Math.round(confidence * 100)}%
                </span>
              </div>
            </summary>
            <div className="procedure-body">
              <h4>Purpose</h4>
              <p>{field(procedure, "purpose", "No purpose was returned.")}</p>
              <h4>Ordered steps</h4>
              <ol>
                {asStrings(procedure.ordered_steps).map((step) => (
                  <li key={step}>{step}</li>
                ))}
              </ol>
              <div className="summary-pair">
                <div>
                  <h4>Functional summary</h4>
                  <p>{field(procedure, "functional_summary", "Not available.")}</p>
                </div>
                <div>
                  <h4>Technical summary</h4>
                  <p>{field(procedure, "technical_summary", "Not available.")}</p>
                </div>
              </div>
            </div>
          </details>
        );
      })}
    </div>
  );
}

function DataOperations({
  metadata,
  procedures,
}: {
  metadata: ApiItem;
  procedures: ApiItem[];
}) {
  const rows: Array<ApiItem & { subprogram?: string }> = [];
  procedures.forEach((procedure) => {
    asArray(procedure.tables).forEach((table) =>
      rows.push({
        ...table,
        subprogram: field(procedure, "subprogram_name", "—"),
      }),
    );
  });
  if (!rows.length) {
    asArray(metadata.tables).forEach((table) => rows.push(table));
  }
  if (!rows.length) {
    return <EmptyResult text="No table or view operations were extracted." />;
  }
  return (
    <div className="data-table-wrap">
      <table className="data-table">
        <thead>
          <tr>
            <th>Subprogram</th>
            <th>Object</th>
            <th>Operation</th>
            <th>Alias</th>
            <th>Dynamic SQL</th>
            <th>Evidence</th>
          </tr>
        </thead>
        <tbody>
          {rows.map((row, index) => {
            const evidence = asArray(row.evidence);
            return (
              <tr key={`${field(row, "name", "table")}-${index}`}>
                <td>{String(row.subprogram ?? "—")}</td>
                <td>
                  <span className="table-object">
                    <Database size={14} />
                    {field(row, "owner", "") &&
                      `${field(row, "owner", "")}.`}
                    {field(row, "name", "Unknown")}
                  </span>
                </td>
                <td>
                  <span className="operation-chip">
                    {field(row, "operation", "READ")}
                  </span>
                </td>
                <td>{field(row, "alias", "—")}</td>
                <td>{field(row, "dynamic_sql", false) ? "Yes" : "No"}</td>
                <td>
                  {evidence.length
                    ? `Line ${field(evidence[0], "start_line", "—")}`
                    : "—"}
                </td>
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
}

function Dependencies({ packageResult }: { packageResult: ApiItem }) {
  const dependencyFlow = asStrings(packageResult.dependency_flow);
  const relationships = asStrings(packageResult.procedure_relationships);
  const items = [...dependencyFlow, ...relationships];
  return items.length ? (
    <div className="dependency-result-grid">
      {items.map((item, index) => (
        <article key={`${item}-${index}`}>
          <span className="dependency-node">
            <GitBranch size={16} />
          </span>
          <p>{item}</p>
        </article>
      ))}
    </div>
  ) : (
    <EmptyResult text="No package dependencies were returned." />
  );
}

function Evidence({
  result,
  source,
}: {
  result: JsonObject;
  source: ApiItem;
}) {
  const retrieval = asArray(result.retrieval);
  return (
    <div className="result-two-column">
      <article className="content-card">
        <div className="content-card-title">
          <ShieldCheck size={17} />
          <h3>Source integrity</h3>
        </div>
        <dl className="definition-list">
          <div>
            <dt>SHA-256</dt>
            <dd>
              <code>{field(source, "sha256", "Not available")}</code>
            </dd>
          </div>
          <div>
            <dt>Source origin</dt>
            <dd>{field(source, "origin", "Current submission")}</dd>
          </div>
        </dl>
      </article>
      <article className="content-card">
        <div className="content-card-title">
          <Database size={17} />
          <h3>Retrieved context</h3>
        </div>
        {retrieval.length ? (
          <ul className="clean-list stacked">
            {retrieval.map((item, index) => (
              <li key={`${field(item, "content_type", "context")}-${index}`}>
                <div>
                  <strong>{field(item, "content_type", "Context")}</strong>
                  <span>{field(item, "review_status", "UNREVIEWED")}</span>
                </div>
                <span className="score">
                  {Math.round(Number(field(item, "score", 0)) * 100)}%
                </span>
              </li>
            ))}
          </ul>
        ) : (
          <p className="empty-copy">
            No prior context exceeded the retrieval threshold.
          </p>
        )}
      </article>
    </div>
  );
}

function EmptyResult({ text }: { text: string }) {
  return (
    <div className="empty-result">
      <ListTree size={24} />
      <p>{text}</p>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

원문에서 계속 ↗

추출 본문 · 출처: dev.to · https://dev.to/iamry02/resultspanel-3ed6

코멘트

답글 남기기