forked from b2894lxlx/tuanwei-admin
feat(ai): 图片模式接入 file-list 参考素材上传(issue 14)
上游「AI 创意助手」开始节点有 file-list 变量 user_files(最多 5 个,允许
image/video/document/audio),但 web 端图片模式此前完全没有附件入口 —— 发送键左侧
那两个图标按钮没有绑定任何事件,是纯装饰。
- 聊天页图片模式新增「参考素材」区:添加附件(隐藏 input[type=file])、已选列表可
逐个移除、上限 5 个(对齐上游 max_length),按 MIME/扩展名判定
image/video/audio/document,发送时组装 file-list 契约
{transfer_method:"local_file", type, upload_file_id, url:""};上传中禁止发送
- uploadAiFile(file, app?) 支持应用代号;gxmu/creativeAssistant 表单页改传
creativeAssistant —— 该页用同一个接口上传却不指定应用,附件必然报
Invalid upload file(既有缺陷,本次一并修掉)
- CreativeAssistantUploadFile.upload_file_id 由 number 改为 string | number:
它实际是 Dify /files/upload 返回的 uuid 字符串,原声明与实现不符
- formatCreativeResult 清洗 video_message / photo_message:剥离 thinking 思维链、
丢弃 {"cover_prompt":…,"width":…} 中间产物 JSON,不再把上游 LLM 的原始输出
当正文展示(截图实测过:气泡里出现整段英文思考过程或裸 JSON)
- 删除发送键左侧两个无事件的装饰性图标按钮(.icon-btn 样式仍被消息操作按钮使用,保留)
This commit is contained in:
@@ -10,10 +10,18 @@ import type {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* AI文件上传
|
* AI文件上传
|
||||||
|
*
|
||||||
|
* @param app 应用代号(后端按代号选择 Dify 应用凭证)。
|
||||||
|
* Dify 的 /files/upload 是**按应用鉴权**的:文件只能被上传时所用应用的工作流引用,
|
||||||
|
* 换个应用引用同一个 upload_file_id 会报 `Invalid upload file`。
|
||||||
|
* 所以喂给创意助手 file-list 变量的附件,必须用 app='creativeAssistant' 上传。
|
||||||
*/
|
*/
|
||||||
export async function uploadAiFile(file: File) {
|
export async function uploadAiFile(file: File, app?: string) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
if (app) {
|
||||||
|
formData.append('app', app);
|
||||||
|
}
|
||||||
const res = await request.post<ApiResult<FileRecord>>(
|
const res = await request.post<ApiResult<FileRecord>>(
|
||||||
MODULES_API_URL + '/ai/file/upload',
|
MODULES_API_URL + '/ai/file/upload',
|
||||||
formData
|
formData
|
||||||
|
|||||||
@@ -3,7 +3,13 @@ import type { FileRecord } from '@/api/system/file/model';
|
|||||||
export interface CreativeAssistantUploadFile {
|
export interface CreativeAssistantUploadFile {
|
||||||
transfer_method: 'local_file';
|
transfer_method: 'local_file';
|
||||||
type: 'image' | 'document' | 'audio' | 'video';
|
type: 'image' | 'document' | 'audio' | 'video';
|
||||||
upload_file_id?: number;
|
/**
|
||||||
|
* Dify /files/upload 返回的文件 id。
|
||||||
|
*
|
||||||
|
* 注意它是 **uuid 字符串**(不是本地 FileRecord 的自增 id);原先声明为 number,
|
||||||
|
* 与实际不符 —— 而且 Dify 按应用鉴权该 id,必须用同一应用的凭证上传才有效。
|
||||||
|
*/
|
||||||
|
upload_file_id?: string | number;
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -480,7 +480,9 @@
|
|||||||
mask: true
|
mask: true
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const result = await uploadAiFile(rawFile);
|
// 必须用创意助手自己的应用凭证上传:Dify 的 /files/upload 按应用鉴权,
|
||||||
|
// 用别的应用的 Key 上传,工作流的 file-list 变量会报 Invalid upload file。
|
||||||
|
const result = await uploadAiFile(rawFile, 'creativeAssistant');
|
||||||
uploadedFiles.value = [
|
uploadedFiles.value = [
|
||||||
...uploadedFiles.value,
|
...uploadedFiles.value,
|
||||||
{
|
{
|
||||||
|
|||||||
+238
-43
@@ -253,6 +253,44 @@
|
|||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="activeChatMode === 'image'" class="ratio-panel attach-panel">
|
||||||
|
<span class="ratio-label">参考素材</span>
|
||||||
|
<button
|
||||||
|
class="ratio-option"
|
||||||
|
type="button"
|
||||||
|
:disabled="loading || uploadingFiles || imageFiles.length >= IMAGE_FILE_LIMIT"
|
||||||
|
@click="pickImageFiles"
|
||||||
|
>
|
||||||
|
{{ uploadingFiles ? "上传中..." : "添加附件" }}
|
||||||
|
</button>
|
||||||
|
<span class="attach-hint">
|
||||||
|
最多 {{ IMAGE_FILE_LIMIT }} 个,支持图片 / 视频 / 音频 / 文档
|
||||||
|
</span>
|
||||||
|
<ul v-if="imageFiles.length" class="attach-list">
|
||||||
|
<li
|
||||||
|
v-for="(item, index) in imageFiles"
|
||||||
|
:key="item.id"
|
||||||
|
class="attach-item"
|
||||||
|
>
|
||||||
|
<span class="attach-name" :title="item.name">{{ item.name }}</span>
|
||||||
|
<button
|
||||||
|
class="attach-remove"
|
||||||
|
type="button"
|
||||||
|
:disabled="loading"
|
||||||
|
@click="removeImageFile(index)"
|
||||||
|
>
|
||||||
|
移除
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<input
|
||||||
|
ref="fileInputRef"
|
||||||
|
class="attach-input"
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
@change="onImageFilesPicked"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
v-model="content"
|
v-model="content"
|
||||||
:placeholder="inputPlaceholder"
|
:placeholder="inputPlaceholder"
|
||||||
@@ -274,39 +312,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="send-actions">
|
<div class="send-actions">
|
||||||
<button class="icon-btn" type="button">
|
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
||||||
<rect
|
|
||||||
x="4"
|
|
||||||
y="6"
|
|
||||||
width="16"
|
|
||||||
height="12"
|
|
||||||
rx="2"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="1.6"
|
|
||||||
/>
|
|
||||||
<circle cx="9" cy="10" r="1.5" fill="currentColor" />
|
|
||||||
<path
|
|
||||||
d="M5 16l4-4 4 4 3-3 3 3"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="1.6"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button class="icon-btn" type="button">
|
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
||||||
<path
|
|
||||||
d="M7 12l6-6a3 3 0 114 4l-7 7a4 4 0 11-6-6l7-7"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="1.6"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
class="send-btn"
|
class="send-btn"
|
||||||
:class="{ 'is-stop': loading }"
|
:class="{ 'is-stop': loading }"
|
||||||
@@ -348,7 +353,8 @@ import {
|
|||||||
WRITING_WORD_COUNT_OPTIONS
|
WRITING_WORD_COUNT_OPTIONS
|
||||||
} from "./writing-params";
|
} from "./writing-params";
|
||||||
import type { WritingOverrides } from "./writing-params";
|
import type { WritingOverrides } from "./writing-params";
|
||||||
import { creativeAssistant } from "@/api/ai/creativeAssistant";
|
import { creativeAssistant, uploadAiFile } from "@/api/ai/creativeAssistant";
|
||||||
|
import type { CreativeAssistantUploadFile } from "@/api/ai/creativeAssistant/model";
|
||||||
import { API_BASE_URL } from "@/config/setting";
|
import { API_BASE_URL } from "@/config/setting";
|
||||||
import type { Menu } from "@/api/system/menu/model";
|
import type { Menu } from "@/api/system/menu/model";
|
||||||
import { useUserStore } from "@/store/modules/user";
|
import { useUserStore } from "@/store/modules/user";
|
||||||
@@ -373,6 +379,13 @@ interface HistorySection {
|
|||||||
items: AiChatList[];
|
items: AiChatList[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 图片模式的参考素材(上传成功后保留 Dify 返回的 file id 与应用内类型) */
|
||||||
|
interface CreativeAttachment {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: CreativeAssistantUploadFile["type"];
|
||||||
|
}
|
||||||
|
|
||||||
interface AuthorityTreeItem extends Menu {
|
interface AuthorityTreeItem extends Menu {
|
||||||
children?: AuthorityTreeItem[];
|
children?: AuthorityTreeItem[];
|
||||||
}
|
}
|
||||||
@@ -392,6 +405,17 @@ const stopping = ref(false);
|
|||||||
const content = ref("");
|
const content = ref("");
|
||||||
const activeChatMode = ref<ChatMode>("chat");
|
const activeChatMode = ref<ChatMode>("chat");
|
||||||
const imageRatio = ref("1:1");
|
const imageRatio = ref("1:1");
|
||||||
|
/**
|
||||||
|
* 图片模式的参考素材,对应 Dify「AI 创意助手」开始节点的 file-list 变量 `user_files`。
|
||||||
|
*
|
||||||
|
* 两个硬约束(都来自上游 `/parameters` 实测):
|
||||||
|
* 1. `max_length = 5`,最多 5 个;
|
||||||
|
* 2. **必须用创意助手自己的应用凭证上传** —— Dify 的 /files/upload 按应用鉴权,
|
||||||
|
* 用别的应用(如 chat)的 Key 上传,工作流引用该 id 会直接报 `Invalid upload file`。
|
||||||
|
*/
|
||||||
|
const imageFiles = ref<CreativeAttachment[]>([]);
|
||||||
|
const uploadingFiles = ref(false);
|
||||||
|
const fileInputRef = ref<HTMLInputElement>();
|
||||||
// 写作模式的显式选择:默认 auto,即由 writing-params 从用户输入里自动识别
|
// 写作模式的显式选择:默认 auto,即由 writing-params 从用户输入里自动识别
|
||||||
const writingDocType = ref("auto");
|
const writingDocType = ref("auto");
|
||||||
const writingWordCount = ref("auto");
|
const writingWordCount = ref("auto");
|
||||||
@@ -424,7 +448,7 @@ const userName = computed(
|
|||||||
const showAdminEntry = computed(() => !isOnlyStudent());
|
const showAdminEntry = computed(() => !isOnlyStudent());
|
||||||
|
|
||||||
const canSend = computed(
|
const canSend = computed(
|
||||||
() => content.value.trim().length > 0 && !loading.value
|
() => content.value.trim().length > 0 && !loading.value && !uploadingFiles.value
|
||||||
);
|
);
|
||||||
|
|
||||||
const isCanceledError = (error: any) =>
|
const isCanceledError = (error: any) =>
|
||||||
@@ -446,6 +470,12 @@ const imageRatioOptions = [
|
|||||||
{ label: "9:16", value: "9:16" }
|
{ label: "9:16", value: "9:16" }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/** 参考素材上限,与 Dify 开始节点 user_files 的 max_length 一致 */
|
||||||
|
const IMAGE_FILE_LIMIT = 5;
|
||||||
|
const IMAGE_FILE_EXTS = ["PNG", "JPG", "JPEG", "GIF", "WEBP", "BMP", "SVG"];
|
||||||
|
const VIDEO_FILE_EXTS = ["MP4", "MOV", "MPEG", "MPGA", "AVI", "M4V"];
|
||||||
|
const AUDIO_FILE_EXTS = ["MP3", "M4A", "WAV", "AMR", "AAC", "OGG", "WEBM"];
|
||||||
|
|
||||||
const inputPlaceholder = computed(() => {
|
const inputPlaceholder = computed(() => {
|
||||||
if (activeChatMode.value === "writing") {
|
if (activeChatMode.value === "writing") {
|
||||||
return "直接描述需求即可,例如:帮我写一篇300字关于迎新晚会的推文,风格活泼,面向大一新生(也可在下方直接指定文体和字数)";
|
return "直接描述需求即可,例如:帮我写一篇300字关于迎新晚会的推文,风格活泼,面向大一新生(也可在下方直接指定文体和字数)";
|
||||||
@@ -1133,6 +1163,26 @@ const formatImageList = (title: string, list: any[] = []) => {
|
|||||||
return `\n\n### ${title}\n${lines.join("\n\n")}`;
|
return `\n\n### ${title}\n${lines.join("\n\n")}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清洗创意助手的 video_message / photo_message。
|
||||||
|
*
|
||||||
|
* 这两个字段在上游其实是 **LLM 节点的原始输出**,实测两种形态都不是给用户看的文案:
|
||||||
|
* ① ` thinking…` 开头的思维链(整段英文推理过程);
|
||||||
|
* ② `{"cover_prompt":"…","width":1024,"height":1024}` 生图提示词中间产物。
|
||||||
|
* 直接展示会让聊天气泡里出现一大段英文思考过程或裸 JSON(2026-10-13 实测截图为证)。
|
||||||
|
* 这里剥离思维链、丢弃提示词 JSON,只有剩下的确实是自然语言时才展示。
|
||||||
|
*/
|
||||||
|
const sanitizeCreativeMessage = (value: string) => {
|
||||||
|
const text = parseAssistantContent(value).content.trim();
|
||||||
|
if (!text) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (text.startsWith("{") && /"(cover_prompt|width|height)"/.test(text)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
||||||
const formatCreativeResult = (payload: unknown) => {
|
const formatCreativeResult = (payload: unknown) => {
|
||||||
// 失败态优先:此时 outputs 为空、answer 是失败文案,不判失败就会落到
|
// 失败态优先:此时 outputs 为空、answer 是失败文案,不判失败就会落到
|
||||||
// 整包兜底把内部标识符渲染出去(见 issue 10)。
|
// 整包兜底把内部标识符渲染出去(见 issue 10)。
|
||||||
@@ -1141,8 +1191,8 @@ const formatCreativeResult = (payload: unknown) => {
|
|||||||
}
|
}
|
||||||
const outputs = getCreativeOutputs(payload as any);
|
const outputs = getCreativeOutputs(payload as any);
|
||||||
const script = pickString(outputs.Script_result, outputs.script, outputs.text);
|
const script = pickString(outputs.Script_result, outputs.script, outputs.text);
|
||||||
const videoMessage = pickString(outputs.video_message);
|
const videoMessage = sanitizeCreativeMessage(pickString(outputs.video_message));
|
||||||
const photoMessage = pickString(outputs.photo_message);
|
const photoMessage = sanitizeCreativeMessage(pickString(outputs.photo_message));
|
||||||
const photoFiles = Array.isArray(outputs.photo_files)
|
const photoFiles = Array.isArray(outputs.photo_files)
|
||||||
? outputs.photo_files
|
? outputs.photo_files
|
||||||
: Array.isArray(outputs.photo_file)
|
: Array.isArray(outputs.photo_file)
|
||||||
@@ -1180,6 +1230,99 @@ const buildImagePrompt = (query: string) => {
|
|||||||
].join("\n");
|
].join("\n");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 按 MIME 或扩展名判定 Dify file-list 要求的 type(image/video/audio/document) */
|
||||||
|
const resolveUploadFileType = (
|
||||||
|
contentType: string,
|
||||||
|
fileName: string
|
||||||
|
): CreativeAssistantUploadFile["type"] => {
|
||||||
|
const mime = String(contentType || "").toLowerCase();
|
||||||
|
if (mime.startsWith("image/")) {
|
||||||
|
return "image";
|
||||||
|
}
|
||||||
|
if (mime.startsWith("video/")) {
|
||||||
|
return "video";
|
||||||
|
}
|
||||||
|
if (mime.startsWith("audio/")) {
|
||||||
|
return "audio";
|
||||||
|
}
|
||||||
|
const ext = String(fileName || "").split(".").pop()?.toUpperCase() || "";
|
||||||
|
if (IMAGE_FILE_EXTS.includes(ext)) {
|
||||||
|
return "image";
|
||||||
|
}
|
||||||
|
if (VIDEO_FILE_EXTS.includes(ext)) {
|
||||||
|
return "video";
|
||||||
|
}
|
||||||
|
if (AUDIO_FILE_EXTS.includes(ext)) {
|
||||||
|
return "audio";
|
||||||
|
}
|
||||||
|
return "document";
|
||||||
|
};
|
||||||
|
|
||||||
|
const pickImageFiles = () => {
|
||||||
|
if (loading.value || uploadingFiles.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fileInputRef.value?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeImageFile = (index: number) => {
|
||||||
|
imageFiles.value.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onImageFilesPicked = async (event: Event) => {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
const picked = Array.from(input.files || []);
|
||||||
|
// 清空 value,否则连续选同一个文件不会再触发 change
|
||||||
|
input.value = "";
|
||||||
|
if (!picked.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const remain = IMAGE_FILE_LIMIT - imageFiles.value.length;
|
||||||
|
if (remain <= 0) {
|
||||||
|
message.warning(`参考素材最多 ${IMAGE_FILE_LIMIT} 个`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const selected = picked.slice(0, remain);
|
||||||
|
if (picked.length > remain) {
|
||||||
|
message.warning(`参考素材最多 ${IMAGE_FILE_LIMIT} 个,本次只上传前 ${remain} 个`);
|
||||||
|
}
|
||||||
|
uploadingFiles.value = true;
|
||||||
|
try {
|
||||||
|
for (const file of selected) {
|
||||||
|
// app 必须传 creativeAssistant:Dify 按应用鉴权上传文件,
|
||||||
|
// 用别的应用凭证上传的 id,工作流会报 Invalid upload file。
|
||||||
|
const record = await uploadAiFile(file, "creativeAssistant");
|
||||||
|
imageFiles.value.push({
|
||||||
|
id: String(record.id),
|
||||||
|
name: record.name || file.name,
|
||||||
|
type: resolveUploadFileType(
|
||||||
|
record.contentType || file.type,
|
||||||
|
record.name || file.name
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
message.success("参考素材已上传");
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e?.message || "参考素材上传失败");
|
||||||
|
} finally {
|
||||||
|
uploadingFiles.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 图片模式入参:user_content 一段文本 + user_files(Dify file-list 变量) */
|
||||||
|
const buildImagePayload = (query: string) => {
|
||||||
|
const userFiles: CreativeAssistantUploadFile[] = imageFiles.value.map((item) => ({
|
||||||
|
transfer_method: "local_file",
|
||||||
|
type: item.type,
|
||||||
|
upload_file_id: item.id,
|
||||||
|
url: ""
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
user_content: buildImagePrompt(query),
|
||||||
|
user_files: userFiles
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const sendModeMessage = async (
|
const sendModeMessage = async (
|
||||||
query: string,
|
query: string,
|
||||||
assistantId: string,
|
assistantId: string,
|
||||||
@@ -1201,12 +1344,7 @@ const sendModeMessage = async (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (activeChatMode.value === "image") {
|
if (activeChatMode.value === "image") {
|
||||||
const imagePrompt = buildImagePrompt(query);
|
const result = await creativeAssistant(buildImagePayload(query), { signal });
|
||||||
const result = await creativeAssistant({
|
|
||||||
user_content: imagePrompt,
|
|
||||||
user_contentString: imagePrompt,
|
|
||||||
user_files: []
|
|
||||||
}, { signal });
|
|
||||||
if (typeof (result as any)?.taskId === "string") {
|
if (typeof (result as any)?.taskId === "string") {
|
||||||
currentTaskId.value = (result as any).taskId;
|
currentTaskId.value = (result as any).taskId;
|
||||||
}
|
}
|
||||||
@@ -1298,6 +1436,10 @@ const onStopGenerate = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onSend = async () => {
|
const onSend = async () => {
|
||||||
|
if (uploadingFiles.value) {
|
||||||
|
message.warning("参考素材正在上传,请稍候");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!canSend.value) {
|
if (!canSend.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1985,6 +2127,59 @@ export default {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.attach-hint {
|
||||||
|
color: #8aa4bd;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attach-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attach-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 1px solid rgba(21, 144, 255, 0.28);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f5fbff;
|
||||||
|
color: #1590ff;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attach-name {
|
||||||
|
max-width: 220px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attach-remove {
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #8aa4bd;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attach-remove:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attach-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.input-shell textarea {
|
.input-shell textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 120px;
|
height: 120px;
|
||||||
|
|||||||
Reference in New Issue
Block a user