feat: add paintaing page
This commit is contained in:
parent
241dcddfed
commit
7401d85825
@ -14,6 +14,7 @@ import AppsPage from './pages/apps/AppsPage'
|
|||||||
import FilesPage from './pages/files/FilesPage'
|
import FilesPage from './pages/files/FilesPage'
|
||||||
import HistoryPage from './pages/history/HistoryPage'
|
import HistoryPage from './pages/history/HistoryPage'
|
||||||
import HomePage from './pages/home/HomePage'
|
import HomePage from './pages/home/HomePage'
|
||||||
|
import PaintingsPage from './pages/paintings/PaintingsPage'
|
||||||
import SettingsPage from './pages/settings/SettingsPage'
|
import SettingsPage from './pages/settings/SettingsPage'
|
||||||
import TranslatePage from './pages/translate/TranslatePage'
|
import TranslatePage from './pages/translate/TranslatePage'
|
||||||
|
|
||||||
@ -30,6 +31,7 @@ function App(): JSX.Element {
|
|||||||
<Route path="/" element={<HomePage />} />
|
<Route path="/" element={<HomePage />} />
|
||||||
<Route path="/files" element={<FilesPage />} />
|
<Route path="/files" element={<FilesPage />} />
|
||||||
<Route path="/agents" element={<AgentsPage />} />
|
<Route path="/agents" element={<AgentsPage />} />
|
||||||
|
<Route path="/paintings" element={<PaintingsPage />} />
|
||||||
<Route path="/translate" element={<TranslatePage />} />
|
<Route path="/translate" element={<TranslatePage />} />
|
||||||
<Route path="/apps" element={<AppsPage />} />
|
<Route path="/apps" element={<AppsPage />} />
|
||||||
<Route path="/messages/*" element={<HistoryPage />} />
|
<Route path="/messages/*" element={<HistoryPage />} />
|
||||||
|
|||||||
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.06667 4.73333C2.06667 3.26057 3.26057 2.06667 4.73333 2.06667H9V1H4.73333C2.67147 1 1 2.67147 1 4.73333V9H2.06667V4.73333ZM2.06667 15.2667C2.06667 16.7394 3.26057 17.9333 4.73333 17.9333H9V19H4.73333C2.67147 19 1 17.3285 1 15.2667V11H2.06667V15.2667ZM15.2667 2.06667C16.7394 2.06667 17.9333 3.26057 17.9333 4.73333V9H19V4.73333C19 2.67147 17.3285 1 15.2667 1H11V2.06667H15.2667ZM17.9333 15.2667C17.9333 16.7394 16.7394 17.9333 15.2667 17.9333H11V19H15.2667C17.3285 19 19 17.3285 19 15.2667V11H17.9333V15.2667Z" fill="#030712"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 683 B |
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.93333 3.73333C4.93333 2.26057 5.978 1.06667 7.26667 1.06667H9V0H7.26667C5.46254 0 4 1.67147 4 3.73333V8H4.93333V3.73333ZM4.93333 16.2667C4.93333 17.7394 5.978 18.9333 7.26667 18.9333H9V20H7.26667C5.46254 20 4 18.3285 4 16.2667V12H4.93333V16.2667ZM13.7333 1.06667C15.022 1.06667 16.0667 2.26057 16.0667 3.73333V8H17V3.73333C17 1.67147 15.5375 0 13.7333 0H12V1.06667H13.7333ZM16.0667 16.2667C16.0667 17.7394 15.022 18.9333 13.7333 18.9333H12V20H13.7333C15.5375 20 17 18.3285 17 16.2667V12H16.0667V16.2667Z" fill="#030712"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 677 B |
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.06667 7.26667C1.06667 5.978 2.26057 4.93333 3.73333 4.93333H8V4H3.73333C1.67147 4 0 5.46254 0 7.26667V9H1.06667V7.26667ZM1.06667 11.2667C1.06667 12.7394 2.26057 13.9333 3.73333 13.9333H8V15H3.73333C1.67147 15 0 13.3285 0 11.2667V10H1.06667V11.2667ZM16.2667 4.93333C17.7394 4.93333 18.9333 5.978 18.9333 7.26667V9H20V7.26667C20 5.46254 18.3285 4 16.2667 4H12V4.93333H16.2667ZM18.9333 11.2667C18.9333 12.7394 17.7394 13.9333 16.2667 13.9333H12V15H16.2667C18.3285 15 20 13.3285 20 11.2667V10H18.9333V11.2667Z" fill="#030712"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 679 B |
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.06667 5.26667C1.06667 3.978 2.26057 2.93333 3.73333 2.93333H8V2H3.73333C1.67147 2 0 3.46254 0 5.26667V9H1.06667V5.26667ZM1.06667 14.7333C1.06667 16.022 2.26057 17.0667 3.73333 17.0667H8V18H3.73333C1.67147 18 0 16.5375 0 14.7333V11H1.06667V14.7333ZM16.2667 2.93333C17.7394 2.93333 18.9333 3.978 18.9333 5.26667V9H20V5.26667C20 3.46254 18.3285 2 16.2667 2H12V2.93333H16.2667ZM18.9333 14.7333C18.9333 16.022 17.7394 17.0667 16.2667 17.0667H12V18H16.2667C18.3285 18 20 16.5375 20 14.7333V11H18.9333V14.7333Z" fill="#030712"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 677 B |
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.93333 3.73333C2.93333 2.26057 3.978 1.06667 5.26667 1.06667H9V0H5.26667C3.46254 0 2 1.67147 2 3.73333V8H2.93333V3.73333ZM2.93333 16.2667C2.93333 17.7394 3.978 18.9333 5.26667 18.9333H9V20H5.26667C3.46254 20 2 18.3285 2 16.2667V12H2.93333V16.2667ZM14.7333 1.06667C16.022 1.06667 17.0667 2.26057 17.0667 3.73333V8H18V3.73333C18 1.67147 16.5375 0 14.7333 0H11V1.06667H14.7333ZM17.0667 16.2667C17.0667 17.7394 16.022 18.9333 14.7333 18.9333H11V20H14.7333C16.5375 20 18 18.3285 18 16.2667V12H17.0667V16.2667Z" fill="#030712"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 677 B |
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.93333 3.73333C5.93333 2.26057 6.978 1.06667 8.26667 1.06667H10V0H8.26667C6.46254 0 5 1.67147 5 3.73333V8H5.93333V3.73333ZM5.93333 16.2667C5.93333 17.7394 6.978 18.9333 8.26667 18.9333H10V20H8.26667C6.46254 20 5 18.3285 5 16.2667V12H5.93333V16.2667ZM12.7333 1.06667C14.022 1.06667 15.0667 2.26057 15.0667 3.73333V8H16V3.73333C16 1.67147 14.5375 0 12.7333 0H11V1.06667H12.7333ZM15.0667 16.2667C15.0667 17.7394 14.022 18.9333 12.7333 18.9333H11V20H12.7333C14.5375 20 16 18.3285 16 16.2667V12H15.0667V16.2667Z" fill="#030712"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 679 B |
@ -1,4 +1,4 @@
|
|||||||
#inputbar .ant-input {
|
#inputbar {
|
||||||
resize: none;
|
resize: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FileSearchOutlined, FolderOutlined, TranslationOutlined } from '@ant-design/icons'
|
import { FileSearchOutlined, FolderOutlined, PictureOutlined, TranslationOutlined } from '@ant-design/icons'
|
||||||
import { isMac } from '@renderer/config/constant'
|
import { isMac } from '@renderer/config/constant'
|
||||||
import { isLocalAi, UserAvatar } from '@renderer/config/env'
|
import { isLocalAi, UserAvatar } from '@renderer/config/env'
|
||||||
import useAvatar from '@renderer/hooks/useAvatar'
|
import useAvatar from '@renderer/hooks/useAvatar'
|
||||||
@ -58,6 +58,11 @@ const Sidebar: FC = () => {
|
|||||||
<i className="iconfont icon-business-smart-assistant" />
|
<i className="iconfont icon-business-smart-assistant" />
|
||||||
</Icon>
|
</Icon>
|
||||||
</StyledLink>
|
</StyledLink>
|
||||||
|
<StyledLink onClick={() => to('/paintings')}>
|
||||||
|
<Icon className={isRoute('/paintings')}>
|
||||||
|
<PictureOutlined style={{ fontSize: 16 }} />
|
||||||
|
</Icon>
|
||||||
|
</StyledLink>
|
||||||
<StyledLink onClick={() => to('/translate')}>
|
<StyledLink onClick={() => to('/translate')}>
|
||||||
<Icon className={isRoute('/translate')}>
|
<Icon className={isRoute('/translate')}>
|
||||||
<TranslationOutlined />
|
<TranslationOutlined />
|
||||||
|
|||||||
@ -930,6 +930,51 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const TEXT_TO_IMAGES_MODELS = [
|
||||||
|
{
|
||||||
|
id: 'black-forest-labs/FLUX.1-dev',
|
||||||
|
provider: 'silicon',
|
||||||
|
name: 'FLUX.1-dev',
|
||||||
|
group: 'FLUX'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'black-forest-labs/FLUX.1-schnell',
|
||||||
|
provider: 'silicon',
|
||||||
|
name: 'FLUX.1-schnell',
|
||||||
|
group: 'FLUX'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Pro/black-forest-labs/FLUX.1-schnell',
|
||||||
|
provider: 'silicon',
|
||||||
|
name: 'FLUX.1-schnell Pro',
|
||||||
|
group: 'FLUX'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'stabilityai/stable-diffusion-3-5-large',
|
||||||
|
provider: 'silicon',
|
||||||
|
name: 'Stable Diffusion 3.5 Large',
|
||||||
|
group: 'Stable Diffusion'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'stabilityai/stable-diffusion-3-medium',
|
||||||
|
provider: 'silicon',
|
||||||
|
name: 'Stable Diffusion 3 Medium',
|
||||||
|
group: 'Stable Diffusion'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'stabilityai/stable-diffusion-2-1',
|
||||||
|
provider: 'silicon',
|
||||||
|
name: 'Stable Diffusion 2.1',
|
||||||
|
group: 'Stable Diffusion'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'stabilityai/stable-diffusion-xl-base-1.0',
|
||||||
|
provider: 'silicon',
|
||||||
|
name: 'Stable Diffusion XL Base 1.0',
|
||||||
|
group: 'Stable Diffusion'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
export function isTextToImageModel(model: Model): boolean {
|
export function isTextToImageModel(model: Model): boolean {
|
||||||
return TEXT_TO_IMAGE_REGEX.test(model.id)
|
return TEXT_TO_IMAGE_REGEX.test(model.id)
|
||||||
}
|
}
|
||||||
|
|||||||
40
src/renderer/src/hooks/usePaintings.ts
Normal file
40
src/renderer/src/hooks/usePaintings.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { TEXT_TO_IMAGES_MODELS } from '@renderer/config/models'
|
||||||
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
|
import { addPainting, removePainting, updatePainting, updatePaintings } from '@renderer/store/paintings'
|
||||||
|
import { Painting } from '@renderer/types'
|
||||||
|
import { uuid } from '@renderer/utils'
|
||||||
|
|
||||||
|
export function usePaintings() {
|
||||||
|
const paintings = useAppSelector((state) => state.paintings.paintings)
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
return {
|
||||||
|
paintings,
|
||||||
|
addPainting: () => {
|
||||||
|
const newPainting: Painting = {
|
||||||
|
id: uuid(),
|
||||||
|
urls: [],
|
||||||
|
files: [],
|
||||||
|
prompt: '',
|
||||||
|
negativePrompt: '',
|
||||||
|
imageSize: '1024x1024',
|
||||||
|
numImages: 1,
|
||||||
|
seed: '',
|
||||||
|
steps: 25,
|
||||||
|
guidanceScale: 4.5,
|
||||||
|
model: TEXT_TO_IMAGES_MODELS[0].id
|
||||||
|
}
|
||||||
|
dispatch(addPainting(newPainting))
|
||||||
|
return newPainting
|
||||||
|
},
|
||||||
|
removePainting: (painting: Painting) => {
|
||||||
|
dispatch(removePainting(painting))
|
||||||
|
},
|
||||||
|
updatePainting: (painting: Painting) => {
|
||||||
|
dispatch(updatePainting(painting))
|
||||||
|
},
|
||||||
|
updatePaintings: (paintings: Painting[]) => {
|
||||||
|
dispatch(updatePaintings(paintings))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -137,6 +137,25 @@
|
|||||||
"stream_output": "Stream Output",
|
"stream_output": "Stream Output",
|
||||||
"search": "Search models..."
|
"search": "Search models..."
|
||||||
},
|
},
|
||||||
|
"images": {
|
||||||
|
"title": "Images",
|
||||||
|
"image.size": "Image Size",
|
||||||
|
"button.new.image": "New Image",
|
||||||
|
"button.delete.image": "Delete Image",
|
||||||
|
"button.delete.image.confirm": "Are you sure you want to delete this image?",
|
||||||
|
"number_images": "Number Images",
|
||||||
|
"number_images_tip": "Number of images to generate (1-4)",
|
||||||
|
"seed": "Seed",
|
||||||
|
"seed_tip": "The same seed and prompt can produce similar images",
|
||||||
|
"inference_steps": "Inference Steps",
|
||||||
|
"inference_steps_tip": "The number of inference steps to perform. More steps produce higher quality but take longer",
|
||||||
|
"guidance_scale": "Guidance Scale",
|
||||||
|
"guidance_scale_tip": "Classifier Free Guidance. How close you want the model to stick to your prompt when looking for a related image to show you",
|
||||||
|
"negative_prompt": "Negative Prompt",
|
||||||
|
"negative_prompt_tip": "Describe what you don't want included in the image",
|
||||||
|
"prompt_placeholder": "Describe the image you want to create, e.g. 'A serene lake at sunset with mountains in the background'",
|
||||||
|
"regenerate.confirm": "This will replace your existing generated images. Do you want to continue?"
|
||||||
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"title": "Files",
|
"title": "Files",
|
||||||
"file": "File",
|
"file": "File",
|
||||||
|
|||||||
1
src/renderer/src/i18n/locales/en.json
Normal file
1
src/renderer/src/i18n/locales/en.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -60,7 +60,7 @@
|
|||||||
"reset.double.confirm.title": "数据丢失!!!",
|
"reset.double.confirm.title": "数据丢失!!!",
|
||||||
"reset.double.confirm.content": "你的全部数据都会丢失,如果没有备份数据,将无法恢复,确定要继续吗?",
|
"reset.double.confirm.content": "你的全部数据都会丢失,如果没有备份数据,将无法恢复,确定要继续吗?",
|
||||||
"upgrade.success.title": "升级成功",
|
"upgrade.success.title": "升级成功",
|
||||||
"upgrade.success.content": "重启应用以完成升级",
|
"upgrade.success.content": "重启用以完成升级",
|
||||||
"upgrade.success.button": "重启",
|
"upgrade.success.button": "重启",
|
||||||
"topic.added": "话题添加成功",
|
"topic.added": "话题添加成功",
|
||||||
"save.success.title": "保存成功"
|
"save.success.title": "保存成功"
|
||||||
@ -137,6 +137,25 @@
|
|||||||
"stream_output": "流式输出",
|
"stream_output": "流式输出",
|
||||||
"search": "搜索模型..."
|
"search": "搜索模型..."
|
||||||
},
|
},
|
||||||
|
"images": {
|
||||||
|
"title": "图片",
|
||||||
|
"image.size": "图片尺寸",
|
||||||
|
"button.new.image": "新建图片",
|
||||||
|
"button.delete.image": "删除图片",
|
||||||
|
"button.delete.image.confirm": "确定要删除此图片吗?",
|
||||||
|
"number_images": "生成数量",
|
||||||
|
"number_images_tip": "一次生成的图片数量 (1-4)",
|
||||||
|
"seed": "随机种子",
|
||||||
|
"seed_tip": "相同的种子和提示词可以生成相似的图片",
|
||||||
|
"inference_steps": "推理步数",
|
||||||
|
"inference_steps_tip": "要执行的推理步数。步数越多,质量越高但耗时越长",
|
||||||
|
"guidance_scale": "引导比例",
|
||||||
|
"guidance_scale_tip": "无分类器指导。控制模型在寻找相关图像时对提示词的遵循程度",
|
||||||
|
"negative_prompt": "反向提示词",
|
||||||
|
"negative_prompt_tip": "描述你不想在图片中出现的内容",
|
||||||
|
"prompt_placeholder": "描述你想创建的图片,例如:'一个宁静的湖泊,夕阳西下,远处是群山'",
|
||||||
|
"regenerate.confirm": "这将覆盖已生成的图片,是否继续?"
|
||||||
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"title": "文件",
|
"title": "文件",
|
||||||
"file": "文件",
|
"file": "文件",
|
||||||
@ -317,7 +336,7 @@
|
|||||||
"new_topic": "新建话题",
|
"new_topic": "新建话题",
|
||||||
"zoom_in": "放大界面",
|
"zoom_in": "放大界面",
|
||||||
"zoom_out": "缩小界面",
|
"zoom_out": "缩小界面",
|
||||||
"zoom_reset": "重置缩放"
|
"zoom_reset": "<EFBFBD><EFBFBD><EFBFBD>置缩放"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"translate": {
|
"translate": {
|
||||||
|
|||||||
@ -137,6 +137,25 @@
|
|||||||
"stream_output": "串流輸出",
|
"stream_output": "串流輸出",
|
||||||
"search": "搜尋模型..."
|
"search": "搜尋模型..."
|
||||||
},
|
},
|
||||||
|
"images": {
|
||||||
|
"title": "繪圖",
|
||||||
|
"image.size": "影像尺寸",
|
||||||
|
"button.new.image": "新繪圖",
|
||||||
|
"button.delete.image": "刪除繪圖",
|
||||||
|
"button.delete.image.confirm": "確定要刪除此繪圖嗎?",
|
||||||
|
"number_images": "生成數量",
|
||||||
|
"number_images_tip": "一次生成的圖片數量 (1-4)",
|
||||||
|
"seed": "隨機種子",
|
||||||
|
"seed_tip": "相同的種子和提示詞可以生成相似的圖片",
|
||||||
|
"inference_steps": "推理步數",
|
||||||
|
"inference_steps_tip": "要執行的推理步數。步數越多,質量越高但耗時越長",
|
||||||
|
"guidance_scale": "引導比例",
|
||||||
|
"guidance_scale_tip": "無分類器指導。控制模型在尋找相關圖像時對提示詞的遵循程度",
|
||||||
|
"negative_prompt": "反向提示詞",
|
||||||
|
"negative_prompt_tip": "描述你不想在圖片中出現的內容",
|
||||||
|
"prompt_placeholder": "描述你想創建的圖片,例如:'一個寧靜的湖泊,夕陽西下,遠處是群山'",
|
||||||
|
"regenerate.confirm": "這將覆蓋已生成的圖片,是否繼續?"
|
||||||
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"title": "檔案",
|
"title": "檔案",
|
||||||
"file": "檔案",
|
"file": "檔案",
|
||||||
|
|||||||
@ -394,7 +394,7 @@ const Textarea = styled(TextArea)`
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-family: Ubuntu;
|
font-family: Ubuntu;
|
||||||
resize: vertical;
|
resize: none !important;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|||||||
@ -8,19 +8,21 @@ import {
|
|||||||
ZoomOutOutlined
|
ZoomOutOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { download } from '@renderer/utils/download'
|
import { download } from '@renderer/utils/download'
|
||||||
import { Image, Space } from 'antd'
|
import { Image as AntImage, ImageProps as AntImageProps, Space } from 'antd'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface ImagePreviewProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
interface ImagePreviewProps extends AntImageProps {
|
||||||
src: string
|
src: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const ImagePreview: React.FC<ImagePreviewProps> = ({ src }) => {
|
const ImagePreview: React.FC<ImagePreviewProps> = ({ src, ...props }) => {
|
||||||
return (
|
return (
|
||||||
<Image
|
<AntImage
|
||||||
src={src}
|
src={src}
|
||||||
|
{...props}
|
||||||
preview={{
|
preview={{
|
||||||
|
mask: typeof props.preview === 'object' ? props.preview.mask : false,
|
||||||
toolbarRender: (
|
toolbarRender: (
|
||||||
_,
|
_,
|
||||||
{
|
{
|
||||||
|
|||||||
112
src/renderer/src/pages/paintings/PaintingsList.tsx
Normal file
112
src/renderer/src/pages/paintings/PaintingsList.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import { DeleteOutlined } from '@ant-design/icons'
|
||||||
|
import DragableList from '@renderer/components/DragableList'
|
||||||
|
import { usePaintings } from '@renderer/hooks/usePaintings'
|
||||||
|
import { Painting } from '@renderer/types'
|
||||||
|
import { classNames } from '@renderer/utils'
|
||||||
|
import { Popconfirm } from 'antd'
|
||||||
|
import { FC, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
interface PaintingsListProps {
|
||||||
|
paintings: Painting[]
|
||||||
|
selectedPainting: Painting
|
||||||
|
onSelectPainting: (painting: Painting) => void
|
||||||
|
onDeletePainting: (painting: Painting) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const PaintingsList: FC<PaintingsListProps> = ({ paintings, selectedPainting, onSelectPainting, onDeletePainting }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [dragging, setDragging] = useState(false)
|
||||||
|
const { updatePaintings } = usePaintings()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container style={{ paddingBottom: dragging ? 30 : 0 }}>
|
||||||
|
<DragableList
|
||||||
|
list={paintings}
|
||||||
|
onUpdate={updatePaintings}
|
||||||
|
onDragStart={() => setDragging(true)}
|
||||||
|
onDragEnd={() => setDragging(false)}>
|
||||||
|
{(item: Painting) => (
|
||||||
|
<CanvasWrapper key={item.id}>
|
||||||
|
<Canvas
|
||||||
|
className={classNames(selectedPainting.id === item.id && 'selected')}
|
||||||
|
onClick={() => onSelectPainting(item)}
|
||||||
|
thumbnail={item.urls[0]}
|
||||||
|
/>
|
||||||
|
<DeleteButton>
|
||||||
|
<Popconfirm
|
||||||
|
title={t('images.button.delete.image.confirm')}
|
||||||
|
onConfirm={() => onDeletePainting(item)}
|
||||||
|
okButtonProps={{ danger: true }}
|
||||||
|
placement="left">
|
||||||
|
<DeleteOutlined />
|
||||||
|
</Popconfirm>
|
||||||
|
</DeleteButton>
|
||||||
|
</CanvasWrapper>
|
||||||
|
)}
|
||||||
|
</DragableList>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 0;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
max-width: 100px;
|
||||||
|
border-left: 0.5px solid var(--color-border);
|
||||||
|
`
|
||||||
|
|
||||||
|
const CanvasWrapper = styled.div`
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.delete-button {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const Canvas = styled.div<{ thumbnail?: string }>`
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
background-color: var(--color-background-soft);
|
||||||
|
background-image: ${(props) => (props.thumbnail ? `url(${props.thumbnail})` : 'none')};
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
border: 1px solid var(--color-background-soft);
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
border: 1px solid var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-background-mute);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const DeleteButton = styled.div.attrs({ className: 'delete-button' })`
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
border-radius: 50%;
|
||||||
|
padding: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-error);
|
||||||
|
background-color: var(--color-background-soft);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default PaintingsList
|
||||||
559
src/renderer/src/pages/paintings/PaintingsPage.tsx
Normal file
559
src/renderer/src/pages/paintings/PaintingsPage.tsx
Normal file
@ -0,0 +1,559 @@
|
|||||||
|
import { PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons'
|
||||||
|
import ImageSize1_1 from '@renderer/assets/images/paintings/image-size-1-1.svg'
|
||||||
|
import ImageSize1_2 from '@renderer/assets/images/paintings/image-size-1-2.svg'
|
||||||
|
import ImageSize3_2 from '@renderer/assets/images/paintings/image-size-3-2.svg'
|
||||||
|
import ImageSize3_4 from '@renderer/assets/images/paintings/image-size-3-4.svg'
|
||||||
|
import ImageSize9_16 from '@renderer/assets/images/paintings/image-size-9-16.svg'
|
||||||
|
import ImageSize16_9 from '@renderer/assets/images/paintings/image-size-16-9.svg'
|
||||||
|
import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar'
|
||||||
|
import { VStack } from '@renderer/components/Layout'
|
||||||
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
|
import { TEXT_TO_IMAGES_MODELS } from '@renderer/config/models'
|
||||||
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
|
import { usePaintings } from '@renderer/hooks/usePaintings'
|
||||||
|
import { useProviders } from '@renderer/hooks/useProvider'
|
||||||
|
import AiProvider from '@renderer/providers/AiProvider'
|
||||||
|
import { getProviderByModel } from '@renderer/services/assistant'
|
||||||
|
import { DEFAULT_PAINTING } from '@renderer/store/paintings'
|
||||||
|
import { Painting } from '@renderer/types'
|
||||||
|
import { getErrorMessage } from '@renderer/utils'
|
||||||
|
import { Button, Input, InputNumber, Radio, Select, Slider, Spin, Tooltip } from 'antd'
|
||||||
|
import TextArea from 'antd/es/input/TextArea'
|
||||||
|
import { FC, useRef, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import SendMessageButton from '../home/Inputbar/SendMessageButton'
|
||||||
|
import ImagePreview from '../home/Markdown/ImagePreview'
|
||||||
|
import { SettingTitle } from '../settings'
|
||||||
|
import PaintingsList from './PaintingsList'
|
||||||
|
|
||||||
|
const IMAGE_SIZES = [
|
||||||
|
{
|
||||||
|
label: '1:1',
|
||||||
|
value: '1024x1024',
|
||||||
|
icon: ImageSize1_1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '1:2',
|
||||||
|
value: '512x1024',
|
||||||
|
icon: ImageSize1_2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '3:2',
|
||||||
|
value: '768x512',
|
||||||
|
icon: ImageSize3_2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '3:4',
|
||||||
|
value: '768x1024',
|
||||||
|
icon: ImageSize3_4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '16:9',
|
||||||
|
value: '1024x576',
|
||||||
|
icon: ImageSize16_9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '9:16',
|
||||||
|
value: '576x1024',
|
||||||
|
icon: ImageSize9_16
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
let _painting: Painting
|
||||||
|
|
||||||
|
const PaintingsPage: FC = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { paintings, addPainting, removePainting, updatePainting } = usePaintings()
|
||||||
|
const [painting, setPainting] = useState<Painting>(_painting || paintings[0])
|
||||||
|
const { theme } = useTheme()
|
||||||
|
const { providers } = useProviders()
|
||||||
|
const siliconProvider = providers.find((p) => p.id === 'silicon')!
|
||||||
|
const [currentImageIndex, setCurrentImageIndex] = useState(0)
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [abortController, setAbortController] = useState<AbortController | null>(null)
|
||||||
|
|
||||||
|
const modelOptions = TEXT_TO_IMAGES_MODELS.map((model) => ({
|
||||||
|
label: model.name,
|
||||||
|
value: model.id
|
||||||
|
}))
|
||||||
|
|
||||||
|
const textareaRef = useRef<any>(null)
|
||||||
|
_painting = painting
|
||||||
|
|
||||||
|
const updatePaintingState = (updates: Partial<Painting>) => {
|
||||||
|
const updatedPainting = { ...painting, ...updates }
|
||||||
|
setPainting(updatedPainting)
|
||||||
|
updatePainting(updatedPainting)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSelectModel = (modelId: string) => {
|
||||||
|
const model = TEXT_TO_IMAGES_MODELS.find((m) => m.id === modelId)
|
||||||
|
if (model) {
|
||||||
|
updatePaintingState({ model: modelId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onGenerate = async () => {
|
||||||
|
if (painting.urls.length > 0) {
|
||||||
|
const confirmed = await window.modal.confirm({
|
||||||
|
content: t('images.regenerate.confirm'),
|
||||||
|
centered: true
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompt = textareaRef.current?.resizableTextArea?.textArea?.value || ''
|
||||||
|
updatePaintingState({ prompt })
|
||||||
|
|
||||||
|
const model = TEXT_TO_IMAGES_MODELS.find((m) => m.id === painting.model)
|
||||||
|
const provider = getProviderByModel(model)
|
||||||
|
|
||||||
|
if (!provider.apiKey) {
|
||||||
|
window.message.error(t('error.no_api_key'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController()
|
||||||
|
setAbortController(controller)
|
||||||
|
setIsLoading(true)
|
||||||
|
const AI = new AiProvider(provider)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const urls = await AI.generateImage({
|
||||||
|
prompt,
|
||||||
|
negativePrompt: painting.negativePrompt || '',
|
||||||
|
imageSize: painting.imageSize || '1024x1024',
|
||||||
|
batchSize: painting.numImages || 1,
|
||||||
|
seed: painting.seed || undefined,
|
||||||
|
numInferenceSteps: painting.steps || 25,
|
||||||
|
guidanceScale: painting.guidanceScale || 4.5,
|
||||||
|
signal: controller.signal
|
||||||
|
})
|
||||||
|
|
||||||
|
if (urls.length > 0) {
|
||||||
|
updatePaintingState({ urls })
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error && error.name !== 'AbortError') {
|
||||||
|
window.message.error(getErrorMessage(error))
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
setAbortController(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
abortController?.abort()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSelectImageSize = (v: string) => {
|
||||||
|
const size = IMAGE_SIZES.find((i) => i.value === v)
|
||||||
|
size && updatePaintingState({ imageSize: size.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextImage = () => {
|
||||||
|
setCurrentImageIndex((prev) => (prev + 1) % painting.urls.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevImage = () => {
|
||||||
|
setCurrentImageIndex((prev) => (prev - 1 + painting.urls.length) % painting.urls.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDeletePainting = (paintingToDelete: Painting) => {
|
||||||
|
if (paintingToDelete.id === painting.id) {
|
||||||
|
const currentIndex = paintings.findIndex((p) => p.id === paintingToDelete.id)
|
||||||
|
|
||||||
|
if (currentIndex > 0) {
|
||||||
|
setPainting(paintings[currentIndex - 1])
|
||||||
|
} else if (paintings.length > 1) {
|
||||||
|
setPainting(paintings[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removePainting(paintingToDelete)
|
||||||
|
|
||||||
|
if (paintings.length === 1) {
|
||||||
|
setPainting(DEFAULT_PAINTING)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSelectPainting = (newPainting: Painting) => {
|
||||||
|
setPainting(newPainting)
|
||||||
|
setCurrentImageIndex(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Navbar>
|
||||||
|
<NavbarCenter style={{ borderRight: 'none' }}>{t('images.title')}</NavbarCenter>
|
||||||
|
<NavbarRight style={{ justifyContent: 'flex-end' }}>
|
||||||
|
<Button size="small" className="nodrag" icon={<PlusOutlined />} onClick={() => setPainting(addPainting())}>
|
||||||
|
{t('images.button.new.image')}
|
||||||
|
</Button>
|
||||||
|
</NavbarRight>
|
||||||
|
</Navbar>
|
||||||
|
<ContentContainer id="content-container">
|
||||||
|
<LeftContainer>
|
||||||
|
<SettingTitle style={{ marginBottom: 5 }}>{t('common.provider')}</SettingTitle>
|
||||||
|
<Select
|
||||||
|
value={siliconProvider.id}
|
||||||
|
disabled={true}
|
||||||
|
options={[{ label: siliconProvider.name, value: siliconProvider.id }]}
|
||||||
|
/>
|
||||||
|
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('common.model')}</SettingTitle>
|
||||||
|
<Select value={painting.model} options={modelOptions} onChange={onSelectModel} />
|
||||||
|
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('images.image.size')}</SettingTitle>
|
||||||
|
<Radio.Group
|
||||||
|
value={painting.imageSize}
|
||||||
|
onChange={(e) => onSelectImageSize(e.target.value)}
|
||||||
|
style={{ display: 'flex' }}>
|
||||||
|
{IMAGE_SIZES.map((size) => (
|
||||||
|
<RadioButton value={size.value} key={size.value}>
|
||||||
|
<VStack alignItems="center">
|
||||||
|
<ImageSizeImage src={size.icon} theme={theme} />
|
||||||
|
<span>{size.label}</span>
|
||||||
|
</VStack>
|
||||||
|
</RadioButton>
|
||||||
|
))}
|
||||||
|
</Radio.Group>
|
||||||
|
|
||||||
|
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
|
||||||
|
{t('images.number_images')}
|
||||||
|
<Tooltip title={t('images.number_images_tip')}>
|
||||||
|
<InfoIcon />
|
||||||
|
</Tooltip>
|
||||||
|
</SettingTitle>
|
||||||
|
<InputNumber
|
||||||
|
min={1}
|
||||||
|
max={4}
|
||||||
|
value={painting.numImages}
|
||||||
|
onChange={(v) => updatePaintingState({ numImages: v || 1 })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
|
||||||
|
{t('images.seed')}
|
||||||
|
<Tooltip title={t('images.seed_tip')}>
|
||||||
|
<InfoIcon />
|
||||||
|
</Tooltip>
|
||||||
|
</SettingTitle>
|
||||||
|
<Input
|
||||||
|
value={painting.seed}
|
||||||
|
onChange={(e) => updatePaintingState({ seed: e.target.value })}
|
||||||
|
suffix={<RefreshIcon onClick={() => updatePaintingState({ seed: '' })} />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
|
||||||
|
{t('images.inference_steps')}
|
||||||
|
<Tooltip title={t('images.inference_steps_tip')}>
|
||||||
|
<InfoIcon />
|
||||||
|
</Tooltip>
|
||||||
|
</SettingTitle>
|
||||||
|
<Slider min={1} max={50} value={painting.steps} onChange={(v) => updatePaintingState({ steps: v })} />
|
||||||
|
<InputNumber
|
||||||
|
min={1}
|
||||||
|
max={50}
|
||||||
|
value={painting.steps}
|
||||||
|
onChange={(v) => updatePaintingState({ steps: v || 25 })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
|
||||||
|
{t('images.guidance_scale')}
|
||||||
|
<Tooltip title={t('images.guidance_scale_tip')}>
|
||||||
|
<InfoIcon />
|
||||||
|
</Tooltip>
|
||||||
|
</SettingTitle>
|
||||||
|
<Slider
|
||||||
|
min={1}
|
||||||
|
max={20}
|
||||||
|
step={0.1}
|
||||||
|
value={painting.guidanceScale}
|
||||||
|
onChange={(v) => updatePaintingState({ guidanceScale: v })}
|
||||||
|
/>
|
||||||
|
<InputNumber
|
||||||
|
min={1}
|
||||||
|
max={20}
|
||||||
|
step={0.1}
|
||||||
|
value={painting.guidanceScale}
|
||||||
|
onChange={(v) => updatePaintingState({ guidanceScale: v || 4.5 })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
|
||||||
|
{t('images.negative_prompt')}
|
||||||
|
<Tooltip title={t('images.negative_prompt_tip')}>
|
||||||
|
<InfoIcon />
|
||||||
|
</Tooltip>
|
||||||
|
</SettingTitle>
|
||||||
|
<TextArea
|
||||||
|
value={painting.negativePrompt}
|
||||||
|
onChange={(e) => updatePaintingState({ negativePrompt: e.target.value })}
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
</LeftContainer>
|
||||||
|
<MainContainer>
|
||||||
|
<Artboard>
|
||||||
|
<LoadingContainer spinning={isLoading}>
|
||||||
|
{painting.urls.length > 0 ? (
|
||||||
|
<ImageContainer>
|
||||||
|
{painting.urls.length > 1 && (
|
||||||
|
<NavigationButton onClick={prevImage} style={{ left: 10 }}>
|
||||||
|
←
|
||||||
|
</NavigationButton>
|
||||||
|
)}
|
||||||
|
<ImagePreview
|
||||||
|
src={painting.urls[currentImageIndex]}
|
||||||
|
preview={{ mask: false }}
|
||||||
|
style={{
|
||||||
|
width: '70vh',
|
||||||
|
height: '70vh',
|
||||||
|
objectFit: 'contain',
|
||||||
|
backgroundColor: 'var(--color-background-soft)',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{painting.urls.length > 1 && (
|
||||||
|
<NavigationButton onClick={nextImage} style={{ right: 10 }}>
|
||||||
|
→
|
||||||
|
</NavigationButton>
|
||||||
|
)}
|
||||||
|
<ImageCounter>
|
||||||
|
{currentImageIndex + 1} / {painting.urls.length}
|
||||||
|
</ImageCounter>
|
||||||
|
</ImageContainer>
|
||||||
|
) : (
|
||||||
|
<ImagePlaceholder />
|
||||||
|
)}
|
||||||
|
{isLoading && (
|
||||||
|
<LoadingOverlay>
|
||||||
|
<Spin size="large" />
|
||||||
|
<CancelButton onClick={onCancel}>{t('common.cancel')}</CancelButton>
|
||||||
|
</LoadingOverlay>
|
||||||
|
)}
|
||||||
|
</LoadingContainer>
|
||||||
|
</Artboard>
|
||||||
|
<InputContainer>
|
||||||
|
<Textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
variant="borderless"
|
||||||
|
disabled={isLoading}
|
||||||
|
value={painting.prompt}
|
||||||
|
onChange={(e) => updatePaintingState({ prompt: e.target.value })}
|
||||||
|
placeholder={t('images.prompt_placeholder')}
|
||||||
|
/>
|
||||||
|
<Toolbar>
|
||||||
|
<ToolbarMenu>
|
||||||
|
<SendMessageButton sendMessage={onGenerate} disabled={isLoading} />
|
||||||
|
</ToolbarMenu>
|
||||||
|
</Toolbar>
|
||||||
|
</InputContainer>
|
||||||
|
</MainContainer>
|
||||||
|
<PaintingsList
|
||||||
|
paintings={paintings}
|
||||||
|
selectedPainting={painting}
|
||||||
|
onSelectPainting={onSelectPainting}
|
||||||
|
onDeletePainting={onDeletePainting}
|
||||||
|
/>
|
||||||
|
</ContentContainer>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ContentContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: row;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
overflow: hidden;
|
||||||
|
`
|
||||||
|
|
||||||
|
const LeftContainer = styled(Scrollbar)`
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
max-width: var(--assistants-width);
|
||||||
|
border-right: 0.5px solid var(--color-border);
|
||||||
|
`
|
||||||
|
|
||||||
|
const MainContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
`
|
||||||
|
|
||||||
|
const Artboard = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
const InputContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100px;
|
||||||
|
max-height: 100px;
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid var(--color-border-soft);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
margin: 0 20px 15px 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Textarea = styled(TextArea)`
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 0;
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
font-family: Ubuntu;
|
||||||
|
resize: none !important;
|
||||||
|
overflow: auto;
|
||||||
|
width: auto;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Toolbar = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 0 8px;
|
||||||
|
padding-bottom: 0;
|
||||||
|
height: 36px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ToolbarMenu = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ImagePlaceholder = styled.div`
|
||||||
|
display: flex;
|
||||||
|
width: 70vh;
|
||||||
|
height: 70vh;
|
||||||
|
background-color: var(--color-background-soft);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ImageSizeImage = styled.img<{ theme: string }>`
|
||||||
|
filter: ${({ theme }) => (theme === 'dark' ? 'invert(100%)' : 'none')};
|
||||||
|
margin-top: 8px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const RadioButton = styled(Radio.Button)`
|
||||||
|
width: 30px;
|
||||||
|
height: 55px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
const InfoIcon = styled(QuestionCircleOutlined)`
|
||||||
|
margin-left: 5px;
|
||||||
|
cursor: help;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
opacity: 0.6;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const RefreshIcon = styled.span`
|
||||||
|
cursor: pointer;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ImageContainer = styled.div`
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.ant-spin {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-spin-spinning {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const NavigationButton = styled(Button)`
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 2;
|
||||||
|
opacity: 0.7;
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const ImageCounter = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
color: white;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const LoadingContainer = styled.div<{ spinning: boolean }>`
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
opacity: ${(props) => (props.spinning ? 0.5 : 1)};
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
`
|
||||||
|
|
||||||
|
const LoadingOverlay = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const CancelButton = styled(Button)`
|
||||||
|
margin-top: 10px;
|
||||||
|
z-index: 1001;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default PaintingsPage
|
||||||
@ -41,4 +41,17 @@ export default class AiProvider {
|
|||||||
public async models(): Promise<OpenAI.Models.Model[]> {
|
public async models(): Promise<OpenAI.Models.Model[]> {
|
||||||
return this.sdk.models()
|
return this.sdk.models()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async generateImage(params: {
|
||||||
|
prompt: string
|
||||||
|
negativePrompt: string
|
||||||
|
imageSize: string
|
||||||
|
batchSize: number
|
||||||
|
seed?: string
|
||||||
|
numInferenceSteps: number
|
||||||
|
guidanceScale: number
|
||||||
|
signal?: AbortSignal
|
||||||
|
}): Promise<string[]> {
|
||||||
|
return this.sdk.generateImage(params)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,4 +41,14 @@ export default abstract class BaseProvider {
|
|||||||
abstract generateText({ prompt, content }: { prompt: string; content: string }): Promise<string>
|
abstract generateText({ prompt, content }: { prompt: string; content: string }): Promise<string>
|
||||||
abstract check(): Promise<{ valid: boolean; error: Error | null }>
|
abstract check(): Promise<{ valid: boolean; error: Error | null }>
|
||||||
abstract models(): Promise<OpenAI.Models.Model[]>
|
abstract models(): Promise<OpenAI.Models.Model[]>
|
||||||
|
abstract generateImage(_params: {
|
||||||
|
prompt: string
|
||||||
|
negativePrompt: string
|
||||||
|
imageSize: string
|
||||||
|
batchSize: number
|
||||||
|
seed?: string
|
||||||
|
numInferenceSteps: number
|
||||||
|
guidanceScale: number
|
||||||
|
signal?: AbortSignal
|
||||||
|
}): Promise<string[]>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -350,4 +350,48 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async generateImage({
|
||||||
|
prompt,
|
||||||
|
negativePrompt,
|
||||||
|
imageSize,
|
||||||
|
batchSize,
|
||||||
|
seed,
|
||||||
|
numInferenceSteps,
|
||||||
|
guidanceScale,
|
||||||
|
signal
|
||||||
|
}: {
|
||||||
|
prompt: string
|
||||||
|
negativePrompt?: string
|
||||||
|
imageSize: string
|
||||||
|
batchSize: number
|
||||||
|
seed?: string
|
||||||
|
numInferenceSteps: number
|
||||||
|
guidanceScale: number
|
||||||
|
signal?: AbortSignal
|
||||||
|
}): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const response = (await this.sdk.request({
|
||||||
|
method: 'post',
|
||||||
|
path: '/images/generations',
|
||||||
|
headers: this.getHeaders(),
|
||||||
|
signal,
|
||||||
|
body: {
|
||||||
|
model: 'stabilityai/stable-diffusion-3-5-large',
|
||||||
|
prompt,
|
||||||
|
negative_prompt: negativePrompt,
|
||||||
|
image_size: imageSize,
|
||||||
|
batch_size: batchSize,
|
||||||
|
seed: seed ? parseInt(seed) : undefined,
|
||||||
|
num_inference_steps: numInferenceSteps,
|
||||||
|
guidance_scale: guidanceScale
|
||||||
|
}
|
||||||
|
})) as { data: Array<{ url: string }> }
|
||||||
|
|
||||||
|
return response.data.map((item) => item.url)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Image generation error:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,14 +7,16 @@ import agents from './agents'
|
|||||||
import assistants from './assistants'
|
import assistants from './assistants'
|
||||||
import llm from './llm'
|
import llm from './llm'
|
||||||
import migrate from './migrate'
|
import migrate from './migrate'
|
||||||
|
import paintings from './paintings'
|
||||||
import runtime from './runtime'
|
import runtime from './runtime'
|
||||||
import settings from './settings'
|
import settings from './settings'
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
assistants,
|
assistants,
|
||||||
settings,
|
|
||||||
llm,
|
|
||||||
agents,
|
agents,
|
||||||
|
paintings,
|
||||||
|
llm,
|
||||||
|
settings,
|
||||||
runtime
|
runtime
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
53
src/renderer/src/store/paintings.ts
Normal file
53
src/renderer/src/store/paintings.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||||
|
import { TEXT_TO_IMAGES_MODELS } from '@renderer/config/models'
|
||||||
|
import { Painting } from '@renderer/types'
|
||||||
|
import { uuid } from '@renderer/utils'
|
||||||
|
|
||||||
|
export interface PaintingsState {
|
||||||
|
paintings: Painting[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_PAINTING: Painting = {
|
||||||
|
id: uuid(),
|
||||||
|
urls: [],
|
||||||
|
files: [],
|
||||||
|
prompt: '',
|
||||||
|
negativePrompt: '',
|
||||||
|
imageSize: '1024x1024',
|
||||||
|
numImages: 1,
|
||||||
|
seed: '',
|
||||||
|
steps: 25,
|
||||||
|
guidanceScale: 4.5,
|
||||||
|
model: TEXT_TO_IMAGES_MODELS[0].id
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: PaintingsState = {
|
||||||
|
paintings: [DEFAULT_PAINTING]
|
||||||
|
}
|
||||||
|
|
||||||
|
const paintingsSlice = createSlice({
|
||||||
|
name: 'paintings',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
updatePaintings: (state, action: PayloadAction<Painting[]>) => {
|
||||||
|
state.paintings = action.payload
|
||||||
|
},
|
||||||
|
addPainting: (state, action: PayloadAction<Painting>) => {
|
||||||
|
state.paintings.push(action.payload)
|
||||||
|
},
|
||||||
|
removePainting: (state, action: PayloadAction<Painting>) => {
|
||||||
|
if (state.paintings.length === 1) {
|
||||||
|
state.paintings = [DEFAULT_PAINTING]
|
||||||
|
} else {
|
||||||
|
state.paintings = state.paintings.filter((c) => c.id !== action.payload.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updatePainting: (state, action: PayloadAction<Painting>) => {
|
||||||
|
state.paintings = state.paintings.map((c) => (c.id === action.payload.id ? action.payload : c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const { updatePaintings, addPainting, removePainting, updatePainting } = paintingsSlice.actions
|
||||||
|
|
||||||
|
export default paintingsSlice.reducer
|
||||||
@ -87,6 +87,20 @@ export type Suggestion = {
|
|||||||
content: string
|
content: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Painting {
|
||||||
|
id: string
|
||||||
|
urls: string[]
|
||||||
|
files: FileType[]
|
||||||
|
prompt?: string
|
||||||
|
negativePrompt?: string
|
||||||
|
imageSize?: string
|
||||||
|
numImages?: number
|
||||||
|
seed?: string
|
||||||
|
steps?: number
|
||||||
|
guidanceScale?: number
|
||||||
|
model?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type MinAppType = {
|
export type MinAppType = {
|
||||||
id?: string | number
|
id?: string | number
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user