This commit is contained in:
@@ -2,6 +2,8 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LocalFileEntry, getLocalFileRawUrl } from '../../api';
|
||||
import { isPreviewable, formatFileDate } from '../../lib/utils';
|
||||
import { splitFileName } from '../../lib/fileType';
|
||||
import { FileTypeIcon } from './FileTypeIcon';
|
||||
|
||||
interface FileBrowserProps {
|
||||
section: 'workspace' | 'input' | 'output' | 'logs';
|
||||
@@ -211,20 +213,30 @@ export function FileBrowser({
|
||||
key={`${entry.kind}:${entry.path}`}
|
||||
className="flex items-center gap-2 px-2.5 py-1.5 rounded-md bg-canvas border border-hairline hover:bg-surface transition-colors"
|
||||
>
|
||||
<span className="text-slate-400 flex-shrink-0" aria-hidden="true">
|
||||
<span className="flex-shrink-0" aria-hidden="true">
|
||||
{entry.kind === 'directory' ? (
|
||||
<svg className="w-4 h-4" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<svg className="w-4 h-4 text-slate-400" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M2 4.5A1.5 1.5 0 013.5 3h3l1.5 2h4.5A1.5 1.5 0 0114 6.5v5A1.5 1.5 0 0112.5 13h-9A1.5 1.5 0 012 11.5v-7z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-4 h-4" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M9 2H4a1.5 1.5 0 00-1.5 1.5v9A1.5 1.5 0 004 14h8a1.5 1.5 0 001.5-1.5V6.5L9 2z" />
|
||||
<path d="M9 2v4.5h4.5" />
|
||||
</svg>
|
||||
<FileTypeIcon name={entry.name} />
|
||||
)}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-[13px] text-slate-800 truncate" title={entry.name}>{entry.name}</div>
|
||||
{entry.kind === 'file'
|
||||
? (() => {
|
||||
// Truncate the stem but never the extension, so the file
|
||||
// type stays readable on long names: `long-na…me.xlsx`.
|
||||
const { stem, ext } = splitFileName(entry.name);
|
||||
return (
|
||||
<div className="flex items-baseline text-[13px] text-slate-800 min-w-0" title={entry.name}>
|
||||
<span className="truncate min-w-0">{stem}</span>
|
||||
{ext && <span className="flex-shrink-0">{ext}</span>}
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
: <div className="text-[13px] text-slate-800 truncate" title={entry.name}>{entry.name}</div>
|
||||
}
|
||||
{entry.kind === 'file' && entry.modifiedAt && (
|
||||
<div className="text-[10px] text-slate-400 font-mono leading-tight">{formatFileDate(entry.modifiedAt)}</div>
|
||||
)}
|
||||
|
||||
@@ -65,6 +65,7 @@ interface FilePreviewProps {
|
||||
section?: string;
|
||||
filePath?: string;
|
||||
editable?: boolean;
|
||||
trustedHtmlUrl?: string;
|
||||
}
|
||||
|
||||
// --- CSV ---
|
||||
@@ -622,7 +623,7 @@ function renderJsonl(content: string): JSX.Element {
|
||||
}
|
||||
|
||||
// --- FilePreview ---
|
||||
export function FilePreview({ name, content, imageSrc, markdownImageBaseUrl, onClose, taskId, section, filePath, editable }: FilePreviewProps) {
|
||||
export function FilePreview({ name, content, imageSrc, markdownImageBaseUrl, onClose, taskId, section, filePath, editable, trustedHtmlUrl }: FilePreviewProps) {
|
||||
const { t } = useTranslation('files');
|
||||
const [mode, setMode] = useState<'view' | 'edit'>('view');
|
||||
const [editContent, setEditContent] = useState(content);
|
||||
@@ -634,6 +635,7 @@ export function FilePreview({ name, content, imageSrc, markdownImageBaseUrl, onC
|
||||
|
||||
const canEdit = editable && taskId != null && section && filePath;
|
||||
const isMarkdownFile = /\.(md|markdown)$/i.test(name);
|
||||
const canOpenTrustedHtml = mode === 'view' && !!trustedHtmlUrl && /\.html?$/i.test(name);
|
||||
|
||||
const handlePrint = async () => {
|
||||
if (printing) return;
|
||||
@@ -756,6 +758,21 @@ export function FilePreview({ name, content, imageSrc, markdownImageBaseUrl, onC
|
||||
<div className="flex justify-between items-center px-4 py-2.5 border-b border-hairline flex-shrink-0 sticky top-0 bg-surface z-10 gap-2">
|
||||
<div className="font-mono text-xs text-slate-700 truncate" title={name}>{name}</div>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
{canOpenTrustedHtml && (
|
||||
<a
|
||||
href={trustedHtmlUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title={t('trustedHtml.tooltip')}
|
||||
className="inline-flex items-center gap-1 px-2.5 h-7 text-2xs font-medium rounded-md border border-amber-300 bg-amber-50 text-amber-800 hover:bg-amber-100 transition-colors"
|
||||
>
|
||||
<svg className="w-3 h-3" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M6 3H3.5A1.5 1.5 0 0 0 2 4.5v8A1.5 1.5 0 0 0 3.5 14h8a1.5 1.5 0 0 0 1.5-1.5V10" />
|
||||
<path d="M9 2h5v5M8 8l6-6" />
|
||||
</svg>
|
||||
{t('trustedHtml.button')}
|
||||
</a>
|
||||
)}
|
||||
{isMarkdownFile && mode === 'view' && (
|
||||
<button
|
||||
onClick={handlePrint}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { fileCategory, categoryColorClass, type FileCategory } from '../../lib/fileType';
|
||||
|
||||
/** カテゴリごとの 16x16 ストロークグリフ (既存ファイル行のアイコンと同じスタイル)。 */
|
||||
function glyph(cat: FileCategory) {
|
||||
switch (cat) {
|
||||
case 'image':
|
||||
return (
|
||||
<>
|
||||
<rect x="2" y="3" width="12" height="10" rx="1.5" />
|
||||
<circle cx="5.5" cy="6.5" r="1" />
|
||||
<path d="M3 12l3-3 2.5 2.5L11 7l2 2" />
|
||||
</>
|
||||
);
|
||||
case 'spreadsheet':
|
||||
return (
|
||||
<>
|
||||
<rect x="2.5" y="3" width="11" height="10" rx="1" />
|
||||
<path d="M2.5 6.5h11M2.5 10h11M6 3v10M10 3v10" />
|
||||
</>
|
||||
);
|
||||
case 'document':
|
||||
return (
|
||||
<>
|
||||
<path d="M9 2H4a1.5 1.5 0 00-1.5 1.5v9A1.5 1.5 0 004 14h8a1.5 1.5 0 001.5-1.5V6.5L9 2z" />
|
||||
<path d="M9 2v4.5h4.5" />
|
||||
<path d="M5.5 9h5M5.5 11h3" />
|
||||
</>
|
||||
);
|
||||
case 'presentation':
|
||||
return (
|
||||
<>
|
||||
<rect x="2" y="3" width="12" height="8" rx="1" />
|
||||
<path d="M8 11v2M6 14h4" />
|
||||
</>
|
||||
);
|
||||
case 'code':
|
||||
return <path d="M6 5L3 8l3 3M10 5l3 3-3 3" />;
|
||||
case 'archive':
|
||||
return (
|
||||
<>
|
||||
<rect x="3" y="3" width="10" height="11" rx="1" />
|
||||
<path d="M8 3v11" strokeDasharray="1.5 1.5" />
|
||||
</>
|
||||
);
|
||||
case 'audio':
|
||||
return (
|
||||
<>
|
||||
<circle cx="5.3" cy="11.5" r="1.7" />
|
||||
<circle cx="10.7" cy="9.7" r="1.7" />
|
||||
<path d="M7 11.5V4l5.4-1.5v7.2" />
|
||||
</>
|
||||
);
|
||||
case 'video':
|
||||
return (
|
||||
<>
|
||||
<rect x="2" y="3.5" width="12" height="9" rx="1.5" />
|
||||
<path d="M7 6.3l3 1.7-3 1.7z" />
|
||||
</>
|
||||
);
|
||||
case 'pdf':
|
||||
case 'other':
|
||||
default:
|
||||
// 折れ角のあるページ。pdf は色 (赤) で識別、other は灰色。
|
||||
return (
|
||||
<>
|
||||
<path d="M9 2H4a1.5 1.5 0 00-1.5 1.5v9A1.5 1.5 0 004 14h8a1.5 1.5 0 001.5-1.5V6.5L9 2z" />
|
||||
<path d="M9 2v4.5h4.5" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function FileTypeIcon({ name, className }: { name: string; className?: string }) {
|
||||
const cat = fileCategory(name);
|
||||
return (
|
||||
<svg
|
||||
className={`${className ?? 'w-4 h-4'} ${categoryColorClass(cat)}`}
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{glyph(cat)}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user