Commit 048729dc authored by 黄同智's avatar 黄同智

基本完成agent 创作的模块内容

parent 4ebe9c71
......@@ -82,6 +82,12 @@ const routes = [
name: 'Agent',
component: Agent,
meta: { title: 'Agent创作', icon: 'audio', affix: true }
},
{
path: 'product-video',
name: 'AgentProductVideo',
component: () => import('@/views/agent/product-video.vue'),
meta: { title: '商品营销视频', hidden: true }
}
]
},
......
This diff is collapsed.
<template>
<main class="flex-1 flex-col">
<div class="flex-1 over-y-auto hide-scroll py-20 px-20">
<div>
<div class="mb-10">
1. 商品参考图
<span class="fs-12 color-888">(上传丰富的参考图有助于提升模型的生成质量)</span>
</div>
<UploadFile v-model="referenceFiles" :width="104" :show-file-list="false" />
</div>
<div class="mt-20">
<div class="fw-600">2. 商品详细信息</div>
<div class="flex gap-12 flex-wrap mt-10">
<div class="base-card px-16 py-10 round-10 bg-fff">
<div class="mb-10">商品名称</div>
<el-input v-model="product.name" class="plain-input" />
</div>
<div class="base-card px-16 py-10 round-10 bg-fff">
<div class="mb-10">商品行业</div>
<el-input v-model="product.industry" class="plain-input" />
</div>
<div class="base-card px-16 py-10 round-10 bg-fff">
<div class="mb-10">商品品类</div>
<el-input v-model="product.category" class="plain-input" />
</div>
</div>
<div class="flex flex-wrap gap-12 mt-20">
<div v-for="section in productSections" :key="section.key"
class="detail-card flex-col p-18 round-10 bg-fff"
:class="{ editing: isSectionEditing(section.key) }">
<div class="flex-between mb-14">
<div class="fs-14 fw-600">{{ section.title }}</div>
<el-icon class="edit-icon pointer" @click.stop="toggleSectionEdit(section.key)">
<Check v-if="isSectionEditing(section.key)" />
<Edit v-else />
</el-icon>
</div>
<template v-if="isSectionEditing(section.key)">
<template v-if="section.items.length">
<div v-for="(_, index) in section.items" :key="`${section.key}-${index}`"
class="editable-row flex-items-center gap-8 h-34 px-8 mb-6 round-4">
<span class="dot flex-center wh-6 round"></span>
<el-input v-model="section.items[index]" class="row-input flex-1" />
<el-icon class="delete-line color-999 pointer"
@click="removeSectionItem(section.key, index)">
<Delete />
</el-icon>
</div>
</template>
<div v-else class="fs-14 color-666 mb-12">暂无{{ section.title }}</div>
<div class="add-row flex-items-center gap-8 mt-8">
<el-input v-model="section.draft" class="add-input flex-1"
:placeholder="`添加${section.title}`" @keyup.enter="addSectionItem(section.key)" />
<el-button type="primary" class="add-btn wh-34 p-0 border-none"
@click="addSectionItem(section.key)">
<el-icon>
<Plus />
</el-icon>
</el-button>
</div>
</template>
<template v-else>
<div v-if="section.items.length" class="view-list flex-col gap-8">
<div v-for="item in section.items" :key="item"
class="view-row flex-items-center gap-8 fs-12 lh-22 color-333">
<span class="dot flex-center wh-6 round"></span>
<span>{{ item }}</span>
</div>
</div>
<div v-else class="fs-14 color-666">暂无{{ section.title }}</div>
</template>
</div>
</div>
</div>
</div>
<footer class="flex justify-end pt-12 pb-12 pr-20" style="border-top: 1px solid #eee;">
<el-button class="fs-12">一键成片</el-button>
<el-button type="primary" class="fs-12 border-none create-btn" @click="emit('createRecord')">
生成创意与分镜
</el-button>
</footer>
</main>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import type { UploadUserFile } from 'element-plus';
import { Check, Delete, Edit, Plus } from '@element-plus/icons-vue';
import UploadFile from '@/components/Upload/index.vue';
type ProductSection = {
key: string;
title: string;
draft: string;
items: string[];
};
const emit = defineEmits<{
createRecord: [];
}>();
const product = reactive({
name: '看看卡萨斯',
industry: '电商零售',
category: '待补充具体品类',
});
const referenceFiles = ref<UploadUserFile[]>([]);
const editingSectionKeys = ref<string[]>([]);
const productSections = reactive<ProductSection[]>([
{
key: 'selling',
title: '商品卖点',
draft: '',
items: ['核心功能突出', '使用简单', '效果直观', '适配日常场景'],
},
{
key: 'pain',
title: '商品痛点',
draft: '',
items: ['现有产品使用体验不佳', '难以直观看到使用效果', '同类商品选择成本高'],
},
{
key: 'target',
title: '目标人群',
draft: '',
items: ['有看看卡萨斯相关使用需求的消费者', '关注实际效果的人群', '重视性价比的用户'],
},
{
key: 'scene',
title: '适用人群和场景',
draft: '',
items: ['日常家庭使用', '需要快速解决问题时使用', '购买同类商品前对比选择'],
},
{
key: 'spec',
title: '商品规格',
draft: '',
items: ['核心功能配置', '便携易用设计', '适配常见使用环境'],
},
{
key: 'discount',
title: '优惠信息',
draft: '',
items: [],
},
]);
const findSection = (key: string) => productSections.find((section) => section.key === key);
const isSectionEditing = (key: string) => editingSectionKeys.value.includes(key);
const toggleSectionEdit = (key: string) => {
if (isSectionEditing(key)) {
editingSectionKeys.value = editingSectionKeys.value.filter((item) => item !== key);
return;
}
editingSectionKeys.value = [...editingSectionKeys.value, key];
};
const addSectionItem = (key: string) => {
const section = findSection(key);
const value = section?.draft.trim();
if (!section || !value) {
return;
}
section.items.push(value);
section.draft = '';
};
const removeSectionItem = (key: string, index: number) => {
const section = findSection(key);
section?.items.splice(index, 1);
};
</script>
<style scoped lang="scss">
.base-card,
.detail-card {
border: 1px solid #dce6f2;
}
.base-card {
flex: 0 0 calc((100% - 12px) / 2);
&:hover {
border-color: rgba(var(--main-color), .6);
}
}
.detail-card {
flex: 0 0 calc((100% - 12px) / 2);
min-height: 158px;
transition: border-color .2s, box-shadow .2s;
&:hover {
border-color: rgba(var(--main-color), .6);
}
&.editing {
border-color: #dce6f2;
}
}
.editable-row {
background: #f8fafc;
&:hover {
background: #f3f6fa;
}
}
.dot {
flex: 0 0 6px;
background: #c8d4e3;
}
.edit-icon {
color: #8aa0bf;
&:hover {
color: #7d28ff;
}
}
.delete-line {
flex: 0 0 18px;
}
.add-row {
border-top: 1px solid #edf2f7;
padding-top: 10px;
}
.add-btn,
.create-btn {
background: #7d28ff;
}
:deep(.plain-input .el-input__wrapper),
:deep(.row-input .el-input__wrapper),
:deep(.add-input .el-input__wrapper) {
padding: 0;
box-shadow: none;
background: transparent;
}
:deep(.plain-input .el-input__inner) {
height: 26px;
font-size: 14px;
color: #0a1b34;
}
:deep(.row-input .el-input__inner),
:deep(.add-input .el-input__inner) {
height: 30px;
font-size: 12px;
}
:deep(.add-input .el-input__wrapper) {
padding: 0 10px;
border: 1px solid #dce6f2;
border-radius: 4px;
background: #fff;
}
</style>
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
<template>
<main class="step4-main flex-1 flex-col hidden">
<section class="flex-1 over-y-auto hide-scroll py-20 px-20">
<div class="video-list flex flex-wrap gap-18">
<div v-for="item in videos" :key="item.id" class="video-card flex-col round-8 hidden bg-fff"
:class="{ active: selectedVideoIds.includes(item.id) }">
<div class="video-cover relative hidden">
<img :src="item.cover" :alt="item.name" class="w-full h-full cover" />
<el-checkbox :model-value="selectedVideoIds.includes(item.id)"
class="select-check absolute top-12 left-12" @change="toggleVideo(item.id)" />
<span v-if="item.recommend"
class="recommend-tag absolute top-10 right-10 h-24 lh-24 px-10 round-4 fs-12 fw-700 color-fff">
推荐
</span>
<span class="duration-tag absolute right-10 bottom-10 h-24 lh-24 px-8 round-4 fs-12 color-fff">
{{ item.duration }}
</span>
</div>
<div class="p-12">
<div class="fs-12 fw-700 color-000 lh-18 ellipsis mb-10">{{ item.name }}</div>
<div class="flex">
<el-button class="detail-btn flex-1 h-34 fs-12">
<el-icon>
<View />
</el-icon>
<span>详情</span>
</el-button>
<el-button class="icon-btn wh-34 p-0">
<el-icon>
<Upload />
</el-icon>
</el-button>
<el-button class="icon-btn wh-34 p-0">
<el-icon>
<Download />
</el-icon>
</el-button>
</div>
</div>
</div>
</div>
</section>
<footer class="flex-between items-center px-20 py-12" style="border-top: 1px solid #eee;">
<div class="flex justify-end">
<el-checkbox v-model="isAllSelected" :indeterminate="isIndeterminate" class="fs-12">全选</el-checkbox>
</div>
<div class="flex-items-center gap-10">
<div class="fs-12 color-888 text-right">
已选择 <span class="color-main fw-700">{{ selectedVideoIds.length }}</span> 个视频
</div>
<el-button type="primary" class="fs-12 border-none create-btn" @click="emit('uploadResource')">
上传资源库
</el-button>
</div>
</footer>
</main>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { Download, Upload, View } from '@element-plus/icons-vue';
import productImg from '@/static/image/huaban.png';
import peopleImg from '@/static/image/meinv.png';
type VideoItem = {
id: string;
name: string;
cover: string;
duration: string;
recommend?: boolean;
};
const emit = defineEmits<{
uploadResource: [];
}>();
const videos: VideoItem[] = [
{
id: 'video-1',
name: '推荐版_玻璃油膜擦营销成片.mp4',
cover: productImg,
duration: '00:28',
recommend: true,
},
{
id: 'video-2',
name: '备选1版_玻璃油膜擦营销成片.mp4',
cover: peopleImg,
duration: '00:30',
},
];
const selectedVideoIds = ref<string[]>(['video-1']);
const isAllSelected = computed({
get: () => selectedVideoIds.value.length === videos.length,
set: (value: boolean) => {
selectedVideoIds.value = value ? videos.map((item) => item.id) : [];
},
});
const isIndeterminate = computed(() => selectedVideoIds.value.length > 0 && selectedVideoIds.value.length < videos.length);
const toggleVideo = (id: string) => {
if (selectedVideoIds.value.includes(id)) {
selectedVideoIds.value = selectedVideoIds.value.filter((item) => item !== id);
return;
}
selectedVideoIds.value = [...selectedVideoIds.value, id];
};
</script>
<style scoped lang="scss">
.step4-main {
min-width: 0;
}
.video-list {
align-content: flex-start;
}
.video-card {
flex: 0 0 260px;
border: 1px solid #dce6f2;
transition: border-color .2s, box-shadow .2s;
&.active {
border-color: rgba(var(--main-color), 1);
box-shadow: 0 0 0 1px rgba(var(--main-color), .35);
}
}
.video-cover {
height: 146px;
background: #f3f6fb;
}
.recommend-tag,
.create-btn {
background: #7d28ff;
}
.duration-tag {
background: rgba(18, 24, 38, .78);
}
.detail-btn,
.icon-btn {
border-color: #dce6f2;
}
:deep(.select-check .el-checkbox__label) {
display: none;
}
:deep(.select-check .el-checkbox__inner) {
width: 18px;
height: 18px;
border-radius: 4px;
}
:deep(.select-check .el-checkbox__inner::after) {
left: 8px;
top: 7px;
}
</style>
......@@ -6,6 +6,14 @@
</el-icon>
<span class="fs-12 fw-600">返回</span>
</div>
<div v-else class="fixed top-20 px-10 right-10 flex-items-center gap-6 pointer" @click="historyDrawerVisible = true">
<el-button>
<el-icon class="fs-16">
<AlarmClock />
</el-icon>
<span class="fs-12 fw-600">历史任务</span>
</el-button>
</div>
<section v-if="!hasMessages">
<div class="mt-60 flex-col items-center">
<div class="flex-center wh-44 round-8 color-fff bg-jb">
......@@ -19,7 +27,8 @@
<el-input v-model="prompt" :rows="10" type="textarea" resize="none" placeholder="发送给 Agent" />
<div class="flex-between pt-16">
<AgentPromptTools />
<div class="border-eee flex bg-fff round-100 p-6 gap-10 pl-16 minw-110 pointer hoverbth" @click="hasMessages = true">
<div class="border-eee flex bg-fff round-100 p-6 gap-10 pl-16 minw-110 pointer hoverbth"
@click="hasMessages = true">
开始创作
<div class="wh-24 color-fff round flex-center bg-jb">
<el-icon>
......@@ -31,9 +40,12 @@
</div>
<div class="flex-center mt-16">
<div class="flex-center gap-10">
<el-button v-for="item in suggestions" :key="item"
class="flex-center h-40 px-20 round-100 border-none boxshaw" @click="applySuggestion(item)">
{{ item }}
<el-button class="flex-center h-40 px-20 round-100 border-none boxshaw" @click="goProductVideo">
用商品拍一条营销视频
</el-button>
<el-button class="flex-center h-40 px-20 round-100 border-none boxshaw"
@click="applySuggestion('参考样片写分镜脚本')">
参考样片写分镜脚本
</el-button>
</div>
</div>
......@@ -104,26 +116,25 @@
</div>
</div>
</section>
<AgentHistoryDrawer v-model="historyDrawerVisible" />
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
import type { Component } from 'vue';
import { ArrowDown, Back, Box, Coin, CopyDocument, Link, MagicStick, Position, Top } from '@element-plus/icons-vue';
import { useRouter } from 'vue-router';
import { AlarmClock, ArrowDown, Back, CopyDocument, MagicStick, Top } from '@element-plus/icons-vue';
import UploadFile from '@/components/Upload/index.vue'
import AgentHistoryDrawer from './components/AgentHistoryDrawer.vue';
import AgentPromptTools from './components/AgentPromptTools.vue';
type ToolItem = {
label: string;
icon: Component;
hasArrow?: boolean;
};
const prompt = ref('');
const router = useRouter();
const submittedTopic = ref('美颜');
const messagesCount = ref(0);
const hasMessages = ref(false)
const historyDrawerVisible = ref(false);
const chatScrollRef = ref<HTMLElement>();
const composerBoxRef = ref<HTMLElement>();
const isInputFocused = ref(false);
......@@ -131,17 +142,6 @@ const isComposerActive = ref(false);
const isNearBottom = ref(true);
const isComposerCompact = computed(() => !isNearBottom.value && !isInputFocused.value && !isComposerActive.value);
const agentControls: ToolItem[] = [
{ label: 'Agent 模式', icon: Position, hasArrow: true },
{ label: '智能模型', icon: Coin },
{ label: '生成参数', icon: MagicStick, hasArrow: true },
{ label: '引用', icon: Link },
{ label: '使用 Skill', icon: Box },
{ label: '优化', icon: MagicStick },
];
const suggestions = ['用商品拍一条营销视频', '参考样片写分镜脚本'];
const sendMessage = () => {
const value = prompt.value.trim();
submittedTopic.value = value || submittedTopic.value;
......@@ -155,6 +155,10 @@ const applySuggestion = (value: string) => {
sendMessage();
};
const goProductVideo = () => {
router.push('/agent/product-video');
};
const handleChatScroll = () => {
if (!chatScrollRef.value) {
return;
......@@ -339,4 +343,5 @@ onBeforeUnmount(() => {
opacity: 1;
}
}
</style>
<template>
<div class="p-20 vh100 hidden">
<el-card class="h-full round-10 content" shadow="never">
<template #header>
<div class="flex-items-center">
<div class="">
<span class="fw-600">{{title}}</span>
</div>
<div class="flex-1 flex-center gap-10 mr-100">
<div v-for="(item, index) in steps" :key="index" class="flex-items-center gap-10 default"
:class="{ finish: item.stepn < currentStep, active: item.stepn === currentStep, select: selectStep === item.stepn }" @click="lookStep(item)">
<div class="flex-items-center px-10 py-6 gap-6 round-6 pointer color-888 name">
<el-icon>
<component :is="item.icon" />
</el-icon>
<span>{{ item.name }}</span>
</div>
<div v-if="index < steps.length-1" class="w-40 line" style="height: 1px;"></div>
</div>
</div>
</div>
</template>
<div class="flex h-full">
<Step1 v-if="selectStep === 1" @create-record="addCreateRecord" />
<Step2 v-else-if="selectStep === 2" @create-preview="addPreviewRecord" />
<Step3 v-else-if="selectStep === 3" @create-video="addVideoRecord" />
<Step4 v-else-if="selectStep === 4" @upload-resource="addUploadRecord" />
<div v-else class="flex-1 flex-center color-888 fs-14">当前步骤内容待生成</div>
<aside class="record-side flex-col bg-fff">
<header class="record-header flex-items-center gap-8 h-48 px-16">
<el-icon class="color-main">
<ChatDotSquare />
</el-icon>
<span class="fs-14 fw-700 color-000">创作记录</span>
</header>
<div class="flex-1 over-y-auto px-18 py-16">
<div class="flex justify-end mb-16">
<div class="user-message color-fff fs-12 lh-20 px-16 py-10 round-8">
用我的「商品」拍一条营销视频
</div>
</div>
<div class="product-mini flex-items-center gap-12 p-12 round-8 mb-20">
<img :src="productImg" alt="看看卡萨斯" class="mini-cover wh-58 round-4 cover" />
<div>
<div class="fs-12 fw-700 color-000 mb-6">看看卡萨斯</div>
<div class="fs-12 color-888">商品 · 共 1 张参考图</div>
</div>
</div>
<div class="assistant-text fs-12 color-333 lh-22 mb-18">
为了帮您制作合适的电商营销视频,请补充具体商品信息,例如商品名称、所属品类、品牌或款式。
</div>
<div class="flex justify-end mb-18">
<div class="user-message color-fff fs-12 lh-20 px-16 py-10 round-8">看看卡萨斯</div>
</div>
<div class="assistant-text fs-12 color-333 lh-22 mb-14">
商品信息已确认,正在为您进行需求分析。
</div>
<div class="record-card flex-items-center gap-12 p-12 round-10 mb-20">
<el-icon class="color-main fs-18">
<Document />
</el-icon>
<div>
<div class="fs-12 fw-700 color-000 mb-4">看看卡萨斯</div>
<div class="fs-10 color-999">查看详情 | 09:12</div>
</div>
</div>
<template v-for="item in chatMessages" :key="item.id">
<div class="assistant-text fs-12 color-333 lh-22 mb-12">
已根据您的要求更新需求分析内容。
</div>
<div class="flex justify-end mb-18">
<div class="user-message color-fff fs-12 lh-20 px-16 py-10 round-8">{{ item.content }}
</div>
</div>
</template>
</div>
<div class="chat-input-wrap p-12">
<div class="chat-input flex-items-center gap-10 p-8 round-8">
<el-input v-model="chatInput" class="chat-text flex-1" type="textarea" resize="none"
:rows="2" placeholder="卡萨丁卡上课的" />
<el-button type="primary" class="send-btn wh-36 p-0 border-none" @click="sendChatMessage">
<el-icon>
<Top />
</el-icon>
</el-button>
</div>
</div>
</aside>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ChatDotSquare, Document, Edit, Top } from '@element-plus/icons-vue';
import productImg from '@/static/image/huaban.png';
import Step1 from './components/Step1.vue';
import Step2 from './components/Step2.vue';
import Step3 from './components/Step3.vue';
import Step4 from './components/Step4.vue';
type ChatMessage = {
id: number;
content: string;
};
const title = ref('商品分析信息')
const currentStep = ref(4)
const selectStep = ref(1)
const steps = ref([
{ name: '需求分析', icon: Edit, stepn: 1, desc: '商品分析信息' },
{ name: '创意与分镜', icon: Edit, stepn: 2, desc: '创意与分镜' },
{ name: '视频预览', icon: Edit, stepn: 3, desc: '视频预览' },
{ name: '视频成片', icon: Edit, stepn: 4, desc: '视频成片' },
])
const lookStep = (item:any)=>{
if(currentStep.value < item.stepn) return
selectStep.value = item.stepn
title.value = item.desc
}
const chatMessages = ref<ChatMessage[]>([
{ id: 1, content: '大活动哈师大' },
{ id: 2, content: '大萨达搬还打算大萨达' },
{ id: 3, content: '大萨达马上到凯撒' },
{ id: 4, content: '大萨达久啊圣诞节啊十八大手机登记登记啊萨' },
{ id: 5, content: '流量卡的角度讲撒娇大萨达' },
]);
const chatInput = ref('');
const sendChatMessage = () => {
const value = chatInput.value.trim();
if (!value) {
return;
}
chatMessages.value.push({
id: Date.now(),
content: value,
});
chatInput.value = '';
};
const addCreateRecord = () => {
currentStep.value = Math.max(currentStep.value, 2);
selectStep.value = 2;
title.value = '创意与分镜';
chatMessages.value.push({
id: Date.now(),
content: '生成创意与分镜',
});
};
const addPreviewRecord = () => {
currentStep.value = Math.max(currentStep.value, 3);
selectStep.value = 3;
title.value = '视频预览';
chatMessages.value.push({
id: Date.now(),
content: '生成视频预览',
});
};
const addVideoRecord = () => {
currentStep.value = Math.max(currentStep.value, 4);
selectStep.value = 4;
title.value = '视频成片';
chatMessages.value.push({
id: Date.now(),
content: '生成视频成片',
});
};
const addUploadRecord = () => {
chatMessages.value.push({
id: Date.now(),
content: '上传资源库',
});
};
</script>
<style scoped lang="scss">
.content {
:deep(.el-card__body) {
padding: 0;
}
}
.default {
.name {
&:hover {
background-color: #eee;
}
}
.line {
background: #ddd;
}
&.finish {
.name {
color: #01b95d;
}
.line {
background: #01b95d;
}
}
&.active {
.name {
color: rgba(var(--main-color), 1);
}
.line {
background: rgba(var(--main-color), 1);
}
}
&.select {
.name {
color: #fff;
background-color: rgba(var(--main-color), 1);
}
.line {
background: rgba(var(--main-color), 1);
}
}
}
.backhover {
&:hover {
color: rgba(var(--main-color), 1);
}
}
.record-card,
.product-mini {
border: 1px solid #dce6f2;
}
.send-btn,
.user-message {
background: #7d28ff;
}
.record-side {
flex: 0 0 30%;
border-left: 1px solid #dce6f2;
}
.record-header {
border-bottom: 1px solid #dce6f2;
}
.product-mini {
width: 250px;
margin-left: auto;
background: #f4f0ff;
}
.mini-cover {
background: #f8fafc;
}
.record-card {
width: 280px;
background: #fbfdff;
}
.chat-input-wrap {
border-top: 1px solid #dce6f2;
}
.chat-input {
border: 1px solid #dce6f2;
background: #fbfdff;
}
:deep(.chat-text .el-textarea__inner) {
padding: 0;
box-shadow: none;
background: transparent;
}
:deep(.chat-text .el-textarea__inner) {
font-size: 12px;
}
</style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment