Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/core/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ declare module 'vue' {
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElUpload: typeof import('element-plus/es')['ElUpload']
ELXButton: typeof import('./src/components/ELXButton/index.vue')['default']
ELXClearButton: typeof import('./src/components/ELXButton/components/ELXClearButton.vue')['default']
ELXLoadingButton: typeof import('./src/components/ELXButton/components/ELXLoadingButton.vue')['default']
ELXSendButton: typeof import('./src/components/ELXButton/components/ELXSendButton.vue')['default']
ELXSpeechButton: typeof import('./src/components/ELXButton/components/ELXSpeechButton.vue')['default']
ELXSpeechLoadingButton: typeof import('./src/components/ELXButton/components/ELXSpeechLoadingButton.vue')['default']
Excel: typeof import('./src/components/FilesCard/fileSvg/excel.vue')['default']
File: typeof import('./src/components/FilesCard/fileSvg/file.vue')['default']
FilesCard: typeof import('./src/components/FilesCard/index.vue')['default']
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export { default as BubbleList } from './components/BubbleList/index.vue';
export { default as ConfigProvider } from './components/ConfigProvider/index.vue';
export { default as Conversations } from './components/Conversations/index.vue';
export { default as EditorSender } from './components/EditorSender/index.vue';
export { default as ELXButton } from './components/ELXButton/index.vue';
export { default as FilesCard } from './components/FilesCard/index.vue';
export { default as MentionSender } from './components/MentionSender/index.vue';
export { default as Prompts } from './components/Prompts/index.vue';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script setup lang="ts">
import { Brush } from '@element-plus/icons-vue';
</script>

<template>
<!-- ClearButton 清理按钮 -->
<el-icon><Brush /></el-icon>
</template>

<style scoped lang="less"></style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<script setup lang="ts"></script>

<template>
<!-- LoadingButton loading 按钮 -->
<svg
class="loading-svg"
viewBox="0 0 1000 1000"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<title>Loading</title>
<rect
fill="currentColor"
height="250"
rx="24"
ry="24"
width="250"
x="375"
y="375"
/>
<circle
cx="500"
cy="500"
fill="none"
r="450"
stroke="currentColor"
stroke-width="100"
opacity="0.45"
/>
<circle
cx="500"
cy="500"
fill="none"
r="450"
stroke="currentColor"
stroke-width="100"
stroke-dasharray="600 9999999"
>
<animateTransform
attributeName="transform"
dur="1s"
from="0 500 500"
repeatCount="indefinite"
to="360 500 500"
type="rotate"
/>
</circle>
Comment on lines +30 to +47
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Replace SMIL with CSS animation for cross‑browser support (Safari).

SMIL <animateTransform> has spotty support (notably Safari). Use CSS keyframes to rotate the arc instead.

Apply:

-    <circle
-      cx="500"
-      cy="500"
-      fill="none"
-      r="450"
-      stroke="currentColor"
-      stroke-width="100"
-      stroke-dasharray="600 9999999"
-    >
-      <animateTransform
-        attributeName="transform"
-        dur="1s"
-        from="0 500 500"
-        repeatCount="indefinite"
-        to="360 500 500"
-        type="rotate"
-      />
-    </circle>
+    <g class="elx-loading-spin" transform-origin="500px 500px">
+      <circle
+        cx="500"
+        cy="500"
+        fill="none"
+        r="450"
+        stroke="currentColor"
+        stroke-width="100"
+        stroke-dasharray="600 9999999"
+      />
+    </g>

And add styles:

-<style scoped lang="less"></style>
+<style scoped lang="less">
+@keyframes elx-spin { to { transform: rotate(360deg); } }
+.elx-loading-spin { animation: elx-spin 1s linear infinite; }
+</style>

Also applies to: 51-51

</svg>
</template>

<style scoped lang="less"></style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script setup lang="ts">
import { Top } from '@element-plus/icons-vue';
</script>

<template>
<!-- SendButton 发送按钮 -->
<el-icon><Top /></el-icon>
</template>

<style scoped lang="less"></style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script setup lang="ts">
import { Microphone } from '@element-plus/icons-vue';
</script>

<template>
<!-- SpeechButton 语音按钮 -->
<el-icon><Microphone /></el-icon>
</template>

<style scoped lang="less"></style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<script setup lang="ts">
withDefaults(defineProps<{ className?: string }>(), {
className: 'loading-svg'
});

// 定义常量
const SIZE = 1000;
const COUNT = 4;
const RECT_WIDTH = 140;
const RECT_RADIUS = RECT_WIDTH / 2;
const RECT_HEIGHT_MIN = 250;
const RECT_HEIGHT_MAX = 500;
const DURATION = 0.8;

// 计算矩形的位置和高度范围
const rects = computed(() => {
const dest = (SIZE - RECT_WIDTH * COUNT) / (COUNT - 1);
return Array.from({ length: COUNT }).map((_, index) => {
const x = index * (dest + RECT_WIDTH);
const yMin = SIZE / 2 - RECT_HEIGHT_MIN / 2;
const yMax = SIZE / 2 - RECT_HEIGHT_MAX / 2;
return { x, yMin, yMax };
});
});
Comment on lines +15 to +24
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Import computed (compile error).

computed is used but not imported.

-<script setup lang="ts">
+<script setup lang="ts">
+import { computed } from 'vue';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 计算矩形的位置和高度范围
const rects = computed(() => {
const dest = (SIZE - RECT_WIDTH * COUNT) / (COUNT - 1);
return Array.from({ length: COUNT }).map((_, index) => {
const x = index * (dest + RECT_WIDTH);
const yMin = SIZE / 2 - RECT_HEIGHT_MIN / 2;
const yMax = SIZE / 2 - RECT_HEIGHT_MAX / 2;
return { x, yMin, yMax };
});
});
<script setup lang="ts">
import { computed } from 'vue';
// 计算矩形的位置和高度范围
const rects = computed(() => {
const dest = (SIZE - RECT_WIDTH * COUNT) / (COUNT - 1);
return Array.from({ length: COUNT }).map((_, index) => {
const x = index * (dest + RECT_WIDTH);
const yMin = SIZE / 2 - RECT_HEIGHT_MIN / 2;
const yMax = SIZE / 2 - RECT_HEIGHT_MAX / 2;
return { x, yMin, yMax };
});
});
</script>
🤖 Prompt for AI Agents
In packages/core/src/components/ELXButton/components/ELXSpeechLoadingButton.vue
around lines 15 to 24, the computed() function is used to build rects but
computed is not imported leading to a compile error; add the computed import
from 'vue' to the component's script imports (e.g., import { computed } from
'vue') and ensure it is included alongside other Vue composition API imports so
the computed usage resolves.

</script>

<template>
<svg
:class="className"
color="currentColor"
:viewBox="`0 0 ${SIZE} ${SIZE}`"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<title>Speech Recording</title>

<template v-for="(item, index) in rects" :key="index">
<rect
fill="currentColor"
:rx="RECT_RADIUS"
:ry="RECT_RADIUS"
:height="RECT_HEIGHT_MIN"
:width="RECT_WIDTH"
:x="item.x"
:y="item.yMin"
>
<animate
attributeName="height"
:values="`${RECT_HEIGHT_MIN}; ${RECT_HEIGHT_MAX}; ${RECT_HEIGHT_MIN}`"
keyTimes="0; 0.5; 1"
:dur="`${DURATION}s`"
:begin="`${(DURATION / COUNT) * index}s`"
repeatCount="indefinite"
/>
<animate
attributeName="y"
:values="`${item.yMin}; ${item.yMax}; ${item.yMin}`"
keyTimes="0; 0.5; 1"
:dur="`${DURATION}s`"
:begin="`${(DURATION / COUNT) * index}s`"
repeatCount="indefinite"
/>
</rect>
</template>
</svg>
</template>

<style scoped lang="less"></style>
60 changes: 60 additions & 0 deletions packages/core/src/components/ELXButton/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { SetupContext } from 'vue';
import { buttonEmits, buttonProps } from 'element-plus';

export const elxButtonTypes = [
'clear',
'loading',
'send',
'speech',
'speechLoading',
''
] as const;

export const elxButtonProps = {
/**
* @description xButton type
*/
elxButtonType: {
type: String,
values: elxButtonTypes,
default: ''
},
...buttonProps
};

export const elxButtonEmits = {
clear: (evt: MouseEvent) => evt instanceof MouseEvent,
cancel: (evt: MouseEvent) => evt instanceof MouseEvent,
submit: (evt: MouseEvent) => evt instanceof MouseEvent,
...buttonEmits
};

export type ELXButtonProps = Partial<ExtractPropTypes<typeof elxButtonProps>>;
export type ELXButtonEmits = typeof elxButtonEmits;
export type ELXButtonType = ELXButtonProps['elxButtonType'];

export function useButton(
props: ELXButtonProps,
emit: SetupContext<ELXButtonEmits>['emit']
) {
const handleClick = (evt: MouseEvent) => {
if (props.disabled || props.loading) {
evt.stopPropagation();
return;
}
if (props.elxButtonType?.trim()?.length) {
if (props.elxButtonType === 'clear') {
return emit('clear', evt);
} else if (['loading', 'speechLoading'].includes(props.elxButtonType)) {
return emit('cancel', evt);
} else if (props.elxButtonType === 'send') {
return emit('submit', evt);
}
}
emit('click', evt);
};

return {
handleClick
};
}
79 changes: 79 additions & 0 deletions packages/core/src/components/ELXButton/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<!-- ClearButton 清理按钮 -->
<script setup lang="ts">
import type { ELXButtonType } from '@components/ELXButton/index.ts';
import {
elxButtonEmits,
elxButtonProps,
useButton
} from '@components/ELXButton/index.ts';
import { ElButton } from 'element-plus';
import { h } from 'vue';
import ELXClearButton from './components/ELXClearButton.vue';
import ELXLoadingButton from './components/ELXLoadingButton.vue';
import ELXSendButton from './components/ELXSendButton.vue';
import ELXSpeechButton from './components/ELXSpeechButton.vue';
import ELXSpeechButtonLoading from './components/ELXSpeechLoadingButton.vue';

const props = defineProps(elxButtonProps);
const emit = defineEmits(elxButtonEmits);
const slots = useSlots();

const { handleClick } = useButton(props, emit);

const elxButtonTypeEmpty = computed(() => {
return props.elxButtonType?.trim()?.length > 0;
});

const classNameVars = computed(() => {
const classNames = ['elx-button'];
if (elxButtonTypeEmpty.value) {
classNames.push(`elx-button-type-${props.elxButtonType}`);
}
return classNames.join(' ');
});

const ELXButtonMap: Record<ELXButtonType & string, Component> = {
clear: ELXClearButton,
loading: ELXLoadingButton,
send: ELXSendButton,
speech: ELXSpeechButton,
speechLoading: ELXSpeechButtonLoading
};

const handleDefaultSlot = computed(() => {
return h(ELXButtonMap[props.elxButtonType] ?? slots.default);
});
Comment on lines +43 to +45
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Potential issue with slot rendering

The computed property returns a VNode from h(), but when slots.default exists, it's already a function that returns VNodes. This could cause rendering issues.

Apply this diff to fix the slot handling:

 const handleDefaultSlot = computed(() => {
-  return h(ELXButtonMap[props.elxButtonType] ?? slots.default);
+  const Component = ELXButtonMap[props.elxButtonType];
+  return Component ? h(Component) : slots.default?.();
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleDefaultSlot = computed(() => {
return h(ELXButtonMap[props.elxButtonType] ?? slots.default);
});
const handleDefaultSlot = computed(() => {
const Component = ELXButtonMap[props.elxButtonType];
return Component ? h(Component) : slots.default?.();
});
🤖 Prompt for AI Agents
In packages/core/src/components/ELXButton/index.vue around lines 43-45, the
computed currently wraps slots.default (a function) with h(), which is
incorrect; instead check if slots.default exists and call it (e.g. return
slots.default() or slots.default?.()) to get its VNodes, otherwise return
h(ELXButtonMap[props.elxButtonType]) to create the mapped component VNode;
update the computed to call the slot function when present and only use h() for
the component fallback.


const coverProps = computed(() => {
let circle = props.circle;
let loading = props.loading;
if (elxButtonTypeEmpty.value) {
circle = true;
loading = false;
}
return {
circle,
loading
};
});
</script>

<template>
<component
:is="
h(
ElButton,
{
...$attrs,
...props,
...coverProps,
class: classNameVars,
onClick: handleClick
},
{ ...$slots, default: () => handleDefaultSlot }
)
"
/>
</template>

<style scoped lang="scss" src="./style.scss"></style>
32 changes: 32 additions & 0 deletions packages/core/src/components/ELXButton/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
.elx-button {
&.elx-button-type-clear {
.el-icon {
// 旋转180
transform: rotate(180deg);
}
}

&.elx-button-type-loading {
padding: 0;

.loading-svg {
color: var(--el-color-primary);
width: 100%;
}
}

&.elx-button-type-send {
}

&.elx-button-type-speech {
}

&.elx-button-type-speechLoading {
padding: 0;

.loading-svg {
color: var(--el-color-primary);
width: 16px;
}
}
}
Loading