Skip to content
Open
2 changes: 1 addition & 1 deletion manual/ja/materials-and-citations.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
| ノートに既にあるリンク | まだ登録されていないリンク(AI チャットの回答がノートに書いたリンクなど)をクリックすると、サイドピークのヘッダーに **素材に登録** ボタンが出る。中身を確かめてから登録すると、その URL は **URL** ギャラリーとグラフに現れる。自動では登録されない |
| スラッシュメニューの **ブックマーク** / **PDF** | URL カードや埋め込みの PDF ビューアを、そのままブロックとして挿入する |
| ギャラリーのボタン | **アップロード**・**URL を追加**・**PDF を追加**・**Word を追加** — ノートに触らずに素材だけ追加する |
| Markdown の取り込み | ノート一覧の **ファイルを取り込む** ボタンから **Markdown (.md) を取り込む** と **Obsidian Vault フォルダを取り込む**。`[[wikilinks]]` は、同時に取り込んだファイル → 既存のノート・ナレッジのタイトルの順でリンクに解決される。解決できなかったリンクは `[[...]]` のテキストのまま残る。Markdown 書き出しもノートへのリンクを `[[wikilinks]]` として書き出すので、書き出したノートを取り込み直してもリンクは保たれる |
| 手元の資料を入れる | サイドバーの **資料を入れる**(空ノートの先頭のチップ、ノート一覧と素材ギャラリーの空状態、ウィンドウのどこかへのフォルダのドロップも同じ)で投入口が開く。Markdown はノートに、PDF・Word・画像・音声・動画・区切りデータは素材になる。それ以外は「対象外」として件数だけ示し、黙って捨てない。Obsidian の vault はそのままで、`[[wikilinks]]` は同時に入れたファイル → 既存のノート・ナレッジのタイトルの順でリンクに解決され、参照している画像も一緒に入る。解決できなかったリンクは `[[...]]` のテキストのまま残る。終わると入ったものの数と、**ノートを検索する**・**つながりを見る**・**AI に聞く** が並ぶ。Markdown 書き出しはノートへのリンクを `[[wikilinks]]` として書き出すので、書き出したノートを入れ直してもリンクは保たれる |
| スマホでの記録 | スマホで撮った写真やメモは受信箱に届く。[スマホでの記録](/ja/mobile)を参照 |

Word ファイルには追加の配慮があります。組み込みのプレビューは、ブラウザが表示できない形式(EMF と TIFF。Excel のグラフを貼った文書によくあります)を変換します。また素材メニューの **埋め込み画像を抽出** は、PDF や Word ファイルの中の画像を取り出して、独立した画像素材として登録します。
Expand Down
2 changes: 1 addition & 1 deletion manual/materials-and-citations.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ There are several routes, all ending in the same library:
| Links already in a note | Clicking a link that is not registered yet — for example one written into the note by an AI chat answer — opens the side peek with an **Add to materials** button in the header. Review the page first, then register it: the URL joins the **URLs** gallery and appears in graphs. Nothing is registered automatically |
| Slash menu **Bookmark** / **PDF** | Insert a URL card or an embedded PDF viewer directly as a block |
| Gallery buttons | **Upload**, **Add URL**, **Add PDF**, **Add Word** — add materials without touching any note |
| Markdown import | In the note list, the **Import files** button offers **Import Markdown (.md)** and **Import Obsidian Vault folder**. `[[wikilinks]]` are resolved to note links — first against files in the same import, then against your existing notes and knowledge entries by title. Unresolved links stay as plain `[[...]]` text. Markdown export writes note links back as `[[wikilinks]]`, so exported notes re-import with their links intact |
| Bring in what you already have | **Bring in files** in the sidebar (also the first chip on an empty note, the empty note list and materials gallery, or simply dropping a folder anywhere in the window) opens one intake sheet. Markdown becomes notes; PDF, Word, images, audio, video and delimited data become materials; anything else is counted as skipped, never dropped silently. Obsidian vaults work as they are: `[[wikilinks]]` are resolved to note links — first against files in the same batch, then against your existing notes and knowledge entries by title — and referenced images come along. Unresolved links stay as plain `[[...]]` text. When it finishes, a report shows what came in and offers **Search your notes**, **See connections** and **Ask AI**. Markdown export writes note links back as `[[wikilinks]]`, so exported notes re-import with their links intact |
| Mobile capture | Photos and memos captured on your phone land in an inbox — see [Mobile](/mobile) |

Word files get extra care: the built-in preview converts formats browsers cannot show (EMF and TIFF, common in documents with pasted Excel charts), and the material menu's **Extract embedded images** pulls the embedded images out of a PDF or Word file and registers them as image materials of their own.
Expand Down
18 changes: 16 additions & 2 deletions src/components/FileSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export type FileSidebarProps = {
onNewNote: () => void;
/** Quick Memo ダイアログを開く(思いつきを 1 行で書き留める入口) */
onNewMemo?: () => void;
/** 投入口(既存資料の一括持ち込み)を開く。手元の資料をそのまま入れる入口。 */
onOpenIntake?: () => void;
/** ヘッダーの「戻る」操作(履歴を 1 段戻す)。canGoBack が false のときは非表示。 */
onBack?: () => void;
/** 戻れる履歴があるか。 */
Expand Down Expand Up @@ -185,6 +187,7 @@ export function FileSidebar({
onSelect,
onNewNote,
onNewMemo,
onOpenIntake,
onBack,
canGoBack,
onRefresh,
Expand Down Expand Up @@ -363,7 +366,9 @@ export function FileSidebar({
ノートに ⌘⇧N を割り当てないのは、ブラウザの「新規シークレットウィンドウ」と衝突して
preventDefault が効かないため。期待を裏切るより、サイドバーボタン経由に一本化する。
「メモ=即速度、ノート=じっくり」の非対称さを UI でも素直に表現する。
パディングは py-1.5 で抑えて、ロゴが視覚的なトップにくるようにヒエラルキーを保つ。 */}
パディングは py-1.5 で抑えて、ロゴが視覚的なトップにくるようにヒエラルキーを保つ。
3 本目(投入口)だけは「作る」ではなく「手元の資料を入れる」動詞。ゼロから書くのでは
なく過去資産の再利用から始めるのが初回の推奨導線なので、常設のボタンとして見せる。 */}
{onNewMemo && (
<button
onClick={onNewMemo}
Expand All @@ -389,10 +394,19 @@ export function FileSidebar({
<button
onClick={onNewNote}
title={t("sidebar.newNoteTooltip")}
className="w-full text-left rounded-lg px-3 py-1.5 text-sm font-medium border border-sidebar-border text-sidebar-foreground/85 bg-transparent hover:bg-sidebar-accent hover:text-sidebar-foreground transition-colors"
className="w-full text-left rounded-lg px-3 py-1.5 mb-1 text-sm font-medium border border-sidebar-border text-sidebar-foreground/85 bg-transparent hover:bg-sidebar-accent hover:text-sidebar-foreground transition-colors"
>
{t("sidebar.newNote")}
</button>
{onOpenIntake && (
<button
onClick={onOpenIntake}
title={t("sidebar.intakeTooltip")}
className="w-full text-left rounded-lg px-3 py-1.5 text-sm font-medium border border-sidebar-border text-sidebar-foreground/85 bg-transparent hover:bg-sidebar-accent hover:text-sidebar-foreground transition-colors"
>
{t("sidebar.intake")}
</button>
)}
</div>

{/* セクション一覧 — IA 第 2 ラウンド E 案(design.md 2026-09-03)。
Expand Down
16 changes: 13 additions & 3 deletions src/features/asset-browser/AssetGalleryView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import type { CitationSource } from "./SelectionPill";
import { UrlBookmarkModal } from "./UrlBookmarkModal";
import { MediaPickerModal } from "./MediaPickerModal";
import { useIsDesktop } from "../../hooks/use-media-query";
import { IntakeReceptacle, type IntakeFile, type IntakeSource } from "../intake";

type SortKey = "uploadedAt" | "name" | "usedIn";

Expand Down Expand Up @@ -455,6 +456,8 @@ export type AssetGalleryViewProps = {
onAddUrlBookmark?: (entry: MediaIndexEntry) => void;
/** ファイル直接アップロード(image/video/audio/pdf/document、ノート非経由) */
onUploadMedia?: (file: File) => Promise<string>;
/** 投入口の受け皿から直接渡されたファイル群を取り込む(素材が 1 件も無いときの初回受け皿用) */
onIntakeFiles?: (files: IntakeFile[], source: IntakeSource) => void;
/** メディアから Knowledge を生成(URL/PDF 用) */
onIngestMedia?: (entry: MediaIndexEntry) => void;
/** URL から PROV ラベル付きノートを生成する(URL エントリー限定) */
Expand Down Expand Up @@ -594,6 +597,7 @@ export function AssetGalleryView({
noteFolderLookup,
onAddUrlBookmark,
onUploadMedia,
onIntakeFiles,
onIngestMedia,
onCreateProvNote,
onTranslatePdf,
Expand Down Expand Up @@ -1578,9 +1582,15 @@ export function AssetGalleryView({
<p className="text-sm text-muted-foreground">{t("common.loading")}</p>
</div>
) : filtered.length === 0 ? (
<div className="flex items-center justify-center py-16">
<p className="text-sm text-muted-foreground">{t("asset.noMedia")}</p>
</div>
onIntakeFiles && mediaIndex && mediaIndex.media.length === 0 ? (
<div className="py-10 max-w-[560px] mx-auto">
<IntakeReceptacle lead={t("intake.emptyMaterialsLead")} onFilesSelected={onIntakeFiles} />
</div>
) : (
<div className="flex items-center justify-center py-16">
<p className="text-sm text-muted-foreground">{t("asset.noMedia")}</p>
</div>
)
) : viewMode === "gallery" ? (
// 列数はコンテナ幅に追従(サイドピークが inline で並ぶと自動で減る)。
// モバイル(overlay 表示)はリフロー不要なので従来どおり 2 列固定
Expand Down
30 changes: 30 additions & 0 deletions src/features/intake/IntakeDropOverlay.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// 全画面ドロップ案内のストーリー
//
// 背景の上に薄い点線の枠と中央カードが重なって見えるかが合否。

import type { Meta, StoryObj } from "@storybook/react-vite";
import { IntakeDropOverlay } from "./IntakeDropOverlay";

const meta: Meta<typeof IntakeDropOverlay> = {
title: "Features/Intake/IntakeDropOverlay",
component: IntakeDropOverlay,
parameters: { layout: "fullscreen" },
};
export default meta;

type Story = StoryObj<typeof IntakeDropOverlay>;

/** 表示中。背景のダミー文章との重なりが分かるようにしている */
export const Visible: Story = {
args: { visible: true },
render: (args) => (
<div style={{ padding: 24 }}>
<h1>ノート一覧</h1>
<p>
ここは背景のダミー文章です。ウィンドウのどこかでファイルをドラッグしている間、
この上に全画面のドロップ案内が重なって表示されます。
</p>
<IntakeDropOverlay {...args} />
</div>
),
};
32 changes: 32 additions & 0 deletions src/features/intake/IntakeDropOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// 全画面ドロップ案内
//
// ウィンドウのどこかにファイルをドラッグしている間だけ、画面全体に「ここに
// 落とせば入る」ことを知らせる。イベントは一切奪わない(pointer-events: none)。
// ドロップの実際の受け取りは useGlobalFileDrop 側が window の drop で行う。

import { createPortal } from "react-dom";
import { FolderInput } from "lucide-react";
import { useT } from "@/i18n";

type IntakeDropOverlayProps = {
visible: boolean;
};

export function IntakeDropOverlay({ visible }: IntakeDropOverlayProps) {
const t = useT();
if (!visible) return null;

return createPortal(
<div className="fixed inset-0 z-[9998] pointer-events-none">
<div className="absolute inset-3 rounded-xl border-2 border-dashed border-primary bg-accent/40" />
<div className="absolute inset-0 flex items-center justify-center">
<div className="rounded-xl bg-background border border-border shadow-lg px-6 py-5 flex flex-col items-center gap-2 text-center">
<FolderInput size={28} className="text-primary" />
<p className="text-sm font-medium">{t("intake.dropOverlay")}</p>
<p className="text-xs text-muted-foreground">{t("intake.rule")}</p>
</div>
</div>
</div>,
document.body,
);
}
137 changes: 137 additions & 0 deletions src/features/intake/IntakeModal.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// 投入口モーダルのストーリー
//
// idle(受け皿) / running(進行中) / done(復元レポート)の 3 状態を並べる。
// 各状態の合否はストーリー先頭のコメントを参照。

import type { Meta, StoryObj } from "@storybook/react-vite";
import { IntakeModal } from "./IntakeModal";

const meta: Meta<typeof IntakeModal> = {
title: "Features/Intake/IntakeModal",
component: IntakeModal,
parameters: { layout: "fullscreen" },
};
export default meta;

type Story = StoryObj<typeof IntakeModal>;

const noop = () => {};

/** 受け皿。「規則の 1 文が読める」「ボタン 2 つの主従が分かる」が合否 */
export const Idle: Story = {
args: {
open: true,
state: { kind: "idle" },
onClose: noop,
onFilesSelected: noop,
},
};

/** dragActive: true。受け皿が強調される */
export const IdleDragging: Story = {
args: {
open: true,
state: { kind: "idle" },
dragActive: true,
onClose: noop,
onFilesSelected: noop,
},
};

/** 進行中。進捗バーとファイル名の表示が既存の importProgress と揃っているかが合否 */
export const Running: Story = {
args: {
open: true,
state: {
kind: "running",
done: 12,
total: 40,
current: "2023-05-14 焼成テスト.md",
failed: [],
},
onClose: noop,
onFilesSelected: noop,
},
};

/** 進行中 + 失敗あり。失敗件数の表示位置が合否 */
export const RunningWithFailures: Story = {
args: {
open: true,
state: {
kind: "running",
done: 30,
total: 40,
current: "2023-05-14 焼成テスト.md",
failed: ["broken.md", "old-notes.md"],
},
onClose: noop,
onFilesSelected: noop,
},
};

/** 復元レポート。数字の一覧と「次の動作」の並びが合否 */
export const Done: Story = {
args: {
open: true,
state: {
kind: "done",
notes: 38,
materials: 17,
linksResolved: 120,
linksUnresolved: 0,
failed: [],
skipped: 0,
aiAvailable: true,
},
onClose: noop,
onFilesSelected: noop,
onSearch: noop,
onShowGraph: noop,
onAskAi: noop,
},
};

/** 復元レポート + 未解決リンク・失敗あり。注意書きが2つとも出るかが合否 */
export const DoneWithIssues: Story = {
args: {
open: true,
state: {
kind: "done",
notes: 36,
materials: 17,
linksResolved: 117,
linksUnresolved: 3,
failed: ["broken.md", "old-notes.md"],
skipped: 4,
aiAvailable: true,
},
onClose: noop,
onFilesSelected: noop,
onSearch: noop,
onShowGraph: noop,
onAskAi: noop,
},
};

/** 復元レポート・AI 未設定。「AI に聞く」の代わりに「AI を設定する」が出るかが合否 */
export const DoneNoAi: Story = {
args: {
open: true,
state: {
kind: "done",
notes: 38,
materials: 17,
linksResolved: 120,
linksUnresolved: 0,
failed: [],
skipped: 0,
aiAvailable: false,
},
onClose: noop,
onFilesSelected: noop,
onSearch: noop,
onShowGraph: noop,
onSetupAi: noop,
},
};
Loading
Loading