summaryrefslogblamecommitdiff
path: root/makima/frontend/src/components/files/FileDetail.tsx
blob: e1fe85f30f1e22f686e8c1167fba2f87a3f89549 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
                                            
                                                                                                   
                                              
                                                                  
 





                                 





                                                                  

                                                                                              

                                                   


                                                 





                                                                           








                                               



                                                                       







                            

                      

                         


                         




                                  







                          


                  



                                                                         

                                                                      















                                                                                                                         

                                                                                         















                                                 




                                           























                                                                                  
                                     
                                                                




















                                                                                                     
                                                   

                          












                                                                                    






























































                                                                                                                                                                      










                                                                            
                
          





                                                                          




                                          


                                                           





                                                             
            

              





                                                                                                                                                    
             








                                                           
 


                                                   












                                                                             




                    



            
import { useState, useEffect } from "react";
import type { FileDetail as FileDetailType, FileVersionSummary, FileVersion } from "../../lib/api";
import { BodyRenderer } from "./BodyRenderer";
import { VersionHistoryDropdown } from "./VersionHistoryDropdown";

export interface FocusedElement {
  index: number;
  type: string;
  preview: string;
}

interface FileDetailProps {
  file: FileDetailType;
  loading: boolean;
  onBack: () => void;
  onSave: (id: string, name: string, description: string) => void;
  onDelete: (id: string) => void;
  onBodyElementUpdate?: (index: number, element: import("../../lib/api").BodyElement) => void;
  onBodyReorder?: (fromIndex: number, toIndex: number) => void;
  onBodyElementDelete?: (index: number) => void;
  onBodyElementDuplicate?: (index: number) => void;
  onEditingChange?: (isEditing: boolean) => void;
  hasPendingRemoteUpdate?: boolean;
  onOverwrite?: () => void;
  // Focus element props
  focusedElement?: FocusedElement | null;
  onFocusElement?: (element: FocusedElement | null) => void;
  onGenerateFromElement?: (index: number, action: string) => void;
  onConvertElement?: (index: number, toType: string) => void;
  onCreateTaskFromElement?: (index: number, selectedText?: string) => void;
  // Version history props
  versions?: FileVersionSummary[];
  versionsLoading?: boolean;
  selectedVersion?: FileVersion | null;
  loadingVersion?: boolean;
  restoring?: boolean;
  onSelectVersion?: (version: number) => void;
  onRestoreVersion?: (version: number) => void;
  onClearVersionSelection?: () => void;
  // Contract context props (for when file is viewed within a contract)
  contractId?: string;
  contractName?: string;
  onContractClick?: () => void;
}

export function FileDetail({
  file,
  loading,
  onBack,
  onSave,
  onDelete,
  onBodyElementUpdate,
  onBodyReorder,
  onBodyElementDelete,
  onBodyElementDuplicate,
  onEditingChange,
  hasPendingRemoteUpdate,
  onOverwrite,
  focusedElement: _focusedElement,
  onFocusElement,
  onGenerateFromElement,
  onConvertElement,
  onCreateTaskFromElement,
  versions = [],
  versionsLoading = false,
  selectedVersion = null,
  loadingVersion = false,
  restoring = false,
  onSelectVersion,
  onRestoreVersion,
  onClearVersionSelection,
  contractId,
  contractName,
  onContractClick,
}: FileDetailProps) {
  const [isEditing, setIsEditing] = useState(false);
  const [name, setName] = useState(file.name);
  const [description, setDescription] = useState(file.description || "");
  const [transcriptExpanded, setTranscriptExpanded] = useState(false);

  // Helper to get element preview text
  const getElementPreview = (index: number): string => {
    const element = file.body[index];
    if (!element) return "";
    switch (element.type) {
      case "heading":
      case "paragraph":
        return element.text.slice(0, 50) + (element.text.length > 50 ? "..." : "");
      case "code":
        return element.content.slice(0, 50) + (element.content.length > 50 ? "..." : "");
      case "list":
        return element.items[0]?.slice(0, 40) + (element.items.length > 1 ? ` (+${element.items.length - 1} more)` : "");
      case "chart":
        return element.title || `${element.chartType} chart`;
      case "image":
        return element.alt || element.caption || "Image";
      case "markdown":
        return element.content.slice(0, 50) + (element.content.length > 50 ? "..." : "");
      default:
        return "Element";
    }
  };

  // Handler for focus action from context menu
  const handleFocusElement = (index: number) => {
    const element = file.body[index];
    if (!element || !onFocusElement) return;
    onFocusElement({
      index,
      type: element.type,
      preview: getElementPreview(index),
    });
  };

  // Update local state when file changes
  useEffect(() => {
    setName(file.name);
    setDescription(file.description || "");
  }, [file.name, file.description]);

  const handleSave = () => {
    onSave(file.id, name, description);
    setIsEditing(false);
  };

  const handleCancel = () => {
    setName(file.name);
    setDescription(file.description || "");
    setIsEditing(false);
  };

  if (loading) {
    return (
      <div className="panel h-full flex items-center justify-center">
        <div className="font-mono text-[#9bc3ff] text-sm">Loading...</div>
      </div>
    );
  }

  return (
    <div className="panel h-full flex flex-col">
      {/* Header */}
      <div className="p-4 border-b border-dashed border-[rgba(117,170,252,0.35)]">
        {/* Breadcrumb navigation */}
        <div className="flex items-center justify-between mb-3">
          <div className="flex items-center gap-2">
            {contractId && contractName ? (
              <>
                <button
                  onClick={onContractClick || onBack}
                  className="font-mono text-xs text-[#75aafc] hover:text-[#9bc3ff] transition-colors"
                >
                  &larr; {contractName}
                </button>
                <span className="font-mono text-xs text-[#556677]">/</span>
                <span className="font-mono text-xs text-[#9bc3ff]">Files</span>
              </>
            ) : (
              <button
                onClick={onBack}
                className="font-mono text-xs text-[#75aafc] hover:text-[#9bc3ff] transition-colors"
              >
                &larr; Back to list
              </button>
            )}
          </div>
          <div className="flex items-center gap-2">
            {isEditing ? (
              <>
                {onSelectVersion && onRestoreVersion && onClearVersionSelection && (
                  <VersionHistoryDropdown
                    currentVersion={file.version}
                    versions={versions}
                    loading={versionsLoading}
                    selectedVersion={selectedVersion}
                    loadingVersion={loadingVersion}
                    onSelectVersion={onSelectVersion}
                    onRestoreVersion={onRestoreVersion}
                    onClearSelection={onClearVersionSelection}
                    restoring={restoring}
                  />
                )}
                <button
                  onClick={handleCancel}
                  className="px-3 py-1.5 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] transition-colors uppercase"
                >
                  Cancel
                </button>
                <button
                  onClick={handleSave}
                  className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase"
                >
                  Save
                </button>
              </>
            ) : (
              <>
                <button
                  onClick={() => setIsEditing(true)}
                  className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] border border-[#0f3c78] hover:border-[#3f6fb3] transition-colors uppercase"
                >
                  Edit
                </button>
                <button
                  onClick={() => onDelete(file.id)}
                  className="px-3 py-1.5 font-mono text-xs text-red-400 border border-red-400/30 hover:border-red-400/50 transition-colors uppercase"
                >
                  Delete
                </button>
              </>
            )}
          </div>
        </div>

        {isEditing ? (
          <div className="space-y-3">
            <input
              type="text"
              value={name}
              onChange={(e) => setName(e.target.value)}
              className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]"
              placeholder="File name"
            />
            <textarea
              value={description}
              onChange={(e) => setDescription(e.target.value)}
              className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc] resize-none"
              rows={2}
              placeholder="Description (optional)"
            />
          </div>
        ) : (
          <>
            <h2 className="font-mono text-lg text-[#dbe7ff] mb-1">
              {file.name}
            </h2>
            {file.description && (
              <p className="font-mono text-sm text-[#9bc3ff]">
                {file.description}
              </p>
            )}
          </>
        )}
      </div>

      {/* Content */}
      <div className="flex-1 overflow-y-auto p-4 space-y-6">
        {/* Summary Section */}
        {file.summary && (
          <div className="border-l-2 border-[#9bc3ff] pl-4">
            <h3 className="font-mono text-xs text-[#75aafc] uppercase mb-2">
              Summary
            </h3>
            <p className="font-mono text-sm text-[#dbe7ff] leading-relaxed">
              {file.summary}
            </p>
          </div>
        )}

        {/* Body Content */}
        <div>
          <h3 className="font-mono text-xs text-[#75aafc] uppercase mb-3">
            Content
          </h3>
          <BodyRenderer
            elements={file.body}
            isEditing={isEditing}
            onUpdate={onBodyElementUpdate}
            onReorder={onBodyReorder}
            onEditingChange={onEditingChange}
            hasPendingRemoteUpdate={hasPendingRemoteUpdate}
            onOverwrite={onOverwrite}
            onFocusElement={handleFocusElement}
            onDeleteElement={onBodyElementDelete}
            onDuplicateElement={onBodyElementDuplicate}
            onConvertElement={onConvertElement}
            onGenerateFromElement={onGenerateFromElement}
            onCreateTaskFromElement={onCreateTaskFromElement}
          />
        </div>

        {/* Collapsible Transcript Section - only show if there are entries */}
        {file.transcript.length > 0 && (
          <div className="border-t border-dashed border-[rgba(117,170,252,0.35)] pt-4">
            <button
              onClick={() => setTranscriptExpanded(!transcriptExpanded)}
              className="flex items-center gap-2 font-mono text-xs text-[#75aafc] hover:text-[#9bc3ff] transition-colors uppercase w-full text-left"
            >
              <span
                className={`transition-transform ${
                  transcriptExpanded ? "rotate-90" : ""
                }`}
              >
                &gt;
              </span>
              Transcript ({file.transcript.length} entries)
            </button>

            {transcriptExpanded && (
              <div className="mt-4 space-y-3 pl-4">
                {file.transcript.map((entry) => (
                  <div key={entry.id} className="font-mono text-sm">
                    <div className="flex items-baseline gap-2 mb-1">
                      <span className="text-[#75aafc] text-xs">
                        [{entry.start.toFixed(2)}s - {entry.end.toFixed(2)}s]
                      </span>
                      <span className="text-[#9bc3ff] text-xs font-bold">
                        {entry.speaker}
                      </span>
                    </div>
                    <p className="m-0 text-[#dbe7ff] leading-relaxed">
                      {entry.text}
                    </p>
                  </div>
                ))}
              </div>
            )}
          </div>
        )}
      </div>
    </div>
  );
}