Merge pull request #50 from LIUBINfighter/image-modal

Image selection modal 图片选取模态框
This commit is contained in:
felix.D 2025-04-27 08:28:40 +08:00 committed by GitHub
commit 6b488e4fc9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 267 additions and 31 deletions

View File

@ -1,4 +1,9 @@
releases: releases:
- version: "0.1.7"
features:
- "Added image selector modal, allowing users to select and upload images in chat"
- "Support for searching and selecting images from Obsidian vault"
- "Change bottom for uploading local image files"
- version: "0.1.6" - version: "0.1.6"
features: features:
- "update model select in chat view, add collected models " - "update model select in chat view, add collected models "

View File

@ -5,6 +5,8 @@
[中文文档](README_zh-CN.md) [中文文档](README_zh-CN.md)
## New Version ## New Version
[0.1.7](https://github.com/infiolab/infio-copilot/releases/tag/0.1.7) Added image selector modal, allowing users to search, select, and upload images in obsidian vault or local file browser
[0.1.6](https://github.com/infiolab/infio-copilot/releases/tag/0.1.6) update apply view, you can edit content in apply view [0.1.6](https://github.com/infiolab/infio-copilot/releases/tag/0.1.6) update apply view, you can edit content in apply view
## Features ## Features
@ -30,7 +32,7 @@ Edit your notes directly within the current file
![inline-edit](asserts/edit-inline.gif) ![inline-edit](asserts/edit-inline.gif)
### chat with vault ### chat with vault
Leverage the power of AI to interact with your entire Obsidian vault, gaining insights and connections across your notes Leverage the power of AI to interact with your entire Obsidian vault, gaining insights and connections across your notes
@ -84,7 +86,7 @@ We value your input and want to ensure you can easily share your thoughts and re
This project stands on the shoulders of giants. We would like to express our gratitude to the following open-source projects: This project stands on the shoulders of giants. We would like to express our gratitude to the following open-source projects:
- [obsidian-copilot-auto-completion](https://github.com/j0rd1smit/obsidian-copilot-auto-completion) - For autocomplete implementation and TypeScript architecture inspiration - [obsidian-copilot-auto-completion](https://github.com/j0rd1smit/obsidian-copilot-auto-completion) - For autocomplete implementation and TypeScript architecture inspiration
- [obsidian-smart-composer](https://github.com/glowingjade/obsidian-smart-composer) - For chat/apply UI patterns and PgLite integration examples - [obsidian-smart-composer](https://github.com/glowingjade/obsidian-smart-composer) - For chat/apply UI patterns and PgLite integration examples
- [continue](https://github.com/continuedev/continue) & [cline](https://github.com/cline/cline) - For prompt engineering and LLM interaction patterns - [continue](https://github.com/continuedev/continue) & [cline](https://github.com/cline/cline) - For prompt engineering and LLM interaction patterns
- [pglite](https://github.com/electric-sql/pglite) - For conversation/vector data storage and sample code - [pglite](https://github.com/electric-sql/pglite) - For conversation/vector data storage and sample code

View File

@ -8,6 +8,8 @@
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/felixduan) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/felixduan)
## 最新版本
[0.1.7](https://github.com/infiolab/infio-copilot/releases/tag/0.1.7) 添加图片选择器模态框,允许用户在聊天中搜索、选择和上传图片
## 功能特点 ## 功能特点

View File

@ -1,7 +1,7 @@
{ {
"id": "infio-copilot", "id": "infio-copilot",
"name": "Infio Copilot", "name": "Infio Copilot",
"version": "0.1.6", "version": "0.1.7",
"minAppVersion": "0.15.0", "minAppVersion": "0.15.0",
"description": "A Cursor-inspired AI assistant for notes that offers smart autocomplete and interactive chat with your selected notes", "description": "A Cursor-inspired AI assistant for notes that offers smart autocomplete and interactive chat with your selected notes",
"author": "Felix.D", "author": "Felix.D",

View File

@ -1,30 +1,40 @@
import { ImageIcon } from 'lucide-react' import { ImageIcon } from 'lucide-react'
import { TFile } from 'obsidian'
import { useApp } from '../../../contexts/AppContext'
import { ImageSelectorModal } from '../../modals/ImageSelectorModal'
export function ImageUploadButton({ export function ImageUploadButton({
onUpload, onUpload,
}: { }: {
onUpload: (files: File[]) => void onUpload: (files: File[]) => void
}) { }) {
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => { const app = useApp()
const files = Array.from(event.target.files ?? [])
if (files.length > 0) { const handleClick = () => {
onUpload(files) const handleVaultImages = async (files: TFile[]) => {
const imageFiles = await Promise.all(
files.map(async (file) => {
const arrayBuffer = await app.vault.readBinary(file)
const blob = new Blob([arrayBuffer], { type: `image/${file.extension}` })
return new File([blob], file.name, { type: `image/${file.extension}` })
})
)
onUpload(imageFiles)
} }
new ImageSelectorModal(app, onUpload, handleVaultImages).open()
} }
return ( return (
<label className="infio-chat-user-input-submit-button"> <button
<input className="infio-chat-user-input-submit-button"
type="file" onClick={handleClick}
accept="image/*" >
multiple
onChange={handleFileChange}
style={{ display: 'none' }}
/>
<div className="infio-chat-user-input-submit-button-icons"> <div className="infio-chat-user-input-submit-button-icons">
<ImageIcon size={12} /> <ImageIcon size={12} />
</div> </div>
<div>Image</div> <div>Image</div>
</label> </button>
) )
} }

View File

@ -0,0 +1,120 @@
import { App, Modal, TFile } from 'obsidian'
import React, { useEffect, useState } from 'react'
import { createRoot } from 'react-dom/client'
type ImageSelectorProps = {
onClose: () => void
onSelectImages: (files: File[]) => void
onSelectVaultImages: (files: TFile[]) => void
app: App
}
const ImageSelector: React.FC<ImageSelectorProps> = ({
onClose,
onSelectImages,
onSelectVaultImages,
app,
}) => {
const [searchTerm, setSearchTerm] = useState('')
const [vaultImages, setVaultImages] = useState<TFile[]>([])
useEffect(() => {
const images = app.vault.getFiles()
.filter((file) => file.extension.match(/png|jpg|jpeg|gif|svg/i))
.filter((file) =>
searchTerm ? file.path.toLowerCase().includes(searchTerm.toLowerCase()) : true
)
setVaultImages(images)
}, [searchTerm, app.vault])
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(event.target.files ?? [])
if (files.length > 0) {
onSelectImages(files)
onClose()
}
}
return (
<div className="infio-image-selector">
<div className="infio-image-selector-header">
<input
type="text"
placeholder="Search images in vault..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="infio-image-search"
/>
<label className="infio-upload-button">
Upload New Image
<input
type="file"
accept="image/*"
multiple
onChange={handleFileUpload}
style={{ display: 'none' }}
/>
</label>
</div>
<div className="infio-image-grid">
{vaultImages.map((file) => (
<div
key={file.path}
className="infio-image-item"
onClick={() => {
onSelectVaultImages([file])
onClose()
}}
>
<img
src={app.vault.adapter.getResourcePath(file.path)}
alt={file.name}
/>
<div className="infio-image-name">{file.name}</div>
</div>
))}
</div>
</div>
)
}
export class ImageSelectorModal extends Modal {
private readonly onSelectImages: (files: File[]) => void
private readonly onSelectVaultImages: (files: TFile[]) => void
constructor(
app: App,
onSelectImages: (files: File[]) => void,
onSelectVaultImages: (files: TFile[]) => void,
) {
super(app)
this.onSelectImages = onSelectImages
this.onSelectVaultImages = onSelectVaultImages
}
onOpen(): void {
const { contentEl, modalEl } = this
// 添加特定的CSS类以便我们可以定位模态框
modalEl.addClass('mod-image-selector')
const root = createRoot(contentEl)
root.render(
<ImageSelector
onClose={() => this.close()}
onSelectImages={this.onSelectImages}
onSelectVaultImages={this.onSelectVaultImages}
app={this.app}
/>
)
}
onClose(): void {
const { contentEl, modalEl } = this
// 移除特定的CSS类
modalEl.removeClass('mod-image-selector')
contentEl.empty()
}
}

View File

@ -1,5 +1,5 @@
/* /*
* Autocomplete * Autocomplete
*/ */
.infio-autocomplete-setting-list-item { .infio-autocomplete-setting-list-item {
@ -62,8 +62,8 @@
} }
} }
/* /*
* Chat * Chat
*/ */
.infio-chat-header { .infio-chat-header {
display: flex; display: flex;
@ -101,7 +101,7 @@
} }
} }
/* /*
* Chat Messages and Content * Chat Messages and Content
* - Message containers * - Message containers
* - Message styling*/ * - Message styling*/
@ -130,7 +130,7 @@
} }
} }
/* /*
* Markdown Content Styling * Markdown Content Styling
* - Typography and text formatting * - Typography and text formatting
* - Lists, blockquotes, and code blocks * - Lists, blockquotes, and code blocks
@ -244,7 +244,7 @@
font-weight: 500; /* 稍微加粗 */ font-weight: 500; /* 稍微加粗 */
} }
/* /*
* Input Controls and Buttons * Input Controls and Buttons
* - Buttons and interactive elements * - Buttons and interactive elements
* - Input areas and textareas * - Input areas and textareas
@ -479,7 +479,7 @@ button:not(.clickable-icon).infio-chat-list-dropdown {
border 0.15s ease-in-out; border 0.15s ease-in-out;
} }
/* /*
* Popovers and Dialogs * Popovers and Dialogs
* - Popover styles * - Popover styles
* - Dialog styles * - Dialog styles
@ -744,7 +744,7 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
margin: 0; margin: 0;
} }
/* /*
* Lexical Content Editable * Lexical Content Editable
* - Styles for the content editable area * - Styles for the content editable area
*/ */
@ -771,7 +771,7 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
line-height: 1.6; line-height: 1.6;
} }
/* /*
* Settings and Dialogs * Settings and Dialogs
* - Styles for settings and dialogs * - Styles for settings and dialogs
*/ */
@ -986,7 +986,7 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
} }
} }
/* /*
* LLM Info * LLM Info
*/ */
.infio-llm-info-content { .infio-llm-info-content {
@ -2036,8 +2036,9 @@ button.infio-chat-input-model-select {
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
user-select: text; user-select: text;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 3; -webkit-line-clamp: 3;
line-clamp: 3;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -2059,10 +2060,10 @@ button.infio-chat-input-model-select {
border: none !important; border: none !important;
box-shadow: none !important; box-shadow: none !important;
color: var(--text-muted); color: var(--text-muted);
padding: 0 !important; padding: 0 !important;
margin: 0 !important; margin: 0 !important;
width: 24px !important; width: 24px !important;
height: 24px !important; height: 24px !important;
&:hover { &:hover {
background-color: var(--background-modifier-hover) !important; background-color: var(--background-modifier-hover) !important;
@ -2148,3 +2149,99 @@ button.infio-chat-input-model-select {
color: var(--text-muted); color: var(--text-muted);
font-size: var(--font-ui-smaller); font-size: var(--font-ui-smaller);
} }
.infio-image-selector {
padding: var(--size-4-3); /* 减小内边距 */
min-width: 450px; /* 减小最小宽度 */
max-width: 700px; /* 减小最大宽度 */
/* 禁用外部容器的滚动 */
overflow: hidden;
/* 确保模态框内容不会超出视口高度 */
max-height: 80vh; /* 减小最大高度 */
display: flex;
flex-direction: column;
}
.infio-image-selector-header {
display: flex;
gap: var(--size-2-1); /* 减小间距 */
margin-bottom: var(--size-4-2); /* 减小底部边距 */
/* 确保头部不会收缩 */
flex-shrink: 0;
}
.infio-image-search {
flex: 1;
padding: var(--size-2-1); /* 减小内边距 */
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-s);
background-color: var(--background-primary);
color: var(--text-normal);
font-size: var(--font-ui-small); /* 减小字体大小 */
height: 28px; /* 固定高度 */
}
.infio-upload-button {
padding: var(--size-2-1) var(--size-4-3); /* 减小内边距 */
background-color: var(--interactive-accent);
color: var(--text-on-accent);
border-radius: var(--radius-s);
cursor: pointer;
font-size: var(--font-ui-smaller); /* 减小字体大小 */
display: inline-flex;
align-items: center;
gap: var(--size-2-1); /* 减小间距 */
height: 28px; /* 固定高度 */
}
.infio-image-grid {
display: grid;
/* 减小每个图片项的最小宽度,使每行可以显示更多图片 */
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: var(--size-4-2); /* 减小间距 */
/* 允许内部容器滚动 */
overflow-y: auto;
padding: var(--size-2-2);
/* 使用flex-grow确保网格占用剩余空间 */
flex-grow: 1;
/* 减小最大高度,使网格更紧凑 */
max-height: calc(70vh - 80px);
}
.infio-image-item {
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-s);
padding: var(--size-2-1); /* 减小内边距 */
cursor: pointer;
transition: all 0.2s ease;
background-color: var(--background-primary);
}
.infio-image-item:hover {
background-color: var(--background-modifier-hover);
}
.infio-image-item img {
width: 100%;
/* 减小图片高度 */
height: 100px;
object-fit: cover;
border-radius: var(--radius-xs);
}
.infio-image-name {
margin-top: var(--size-2-1); /* 减小上边距 */
font-size: var(--font-ui-smallest); /* 使用更小的字体 */
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text-muted); /* 使用更淡的颜色 */
line-height: 1.2; /* 减小行高 */
}
/* 禁用Obsidian模态框的滚动 */
.modal.mod-image-selector .modal-content {
overflow: hidden !important;
}