Skip to content

feat: AI dialog box, left mouse button menu #2005

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 9, 2025
Merged
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
3 changes: 2 additions & 1 deletion ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@
"vue-clipboard3": "^2.0.0",
"vue-codemirror": "^6.1.1",
"vue-i18n": "^9.13.1",
"vue-router": "^4.2.4"
"vue-router": "^4.2.4",
"vue3-menus": "^1.1.2"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.3.2",
Expand Down
10 changes: 9 additions & 1 deletion ui/src/components/ai-chat/component/answer-content/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<img v-if="application.avatar" :src="application.avatar" height="32px" width="32px" />
<LogoIcon v-else height="32px" width="32px" />
</div>
<div class="content">
<div class="content" @click.stop @mouseup="openControl">
<el-card shadow="always" class="dialog-card mb-8">
<MdRenderer
v-if="
Expand Down Expand Up @@ -56,6 +56,7 @@ import MdRenderer from '@/components/markdown/MdRenderer.vue'
import OperationButton from '@/components/ai-chat/component/operation-button/index.vue'
import { type chatType } from '@/api/type/application'
import { computed } from 'vue'
import bus from '@/bus'
const props = defineProps<{
chatRecord: chatType
application: any
Expand All @@ -79,6 +80,13 @@ const chatMessage = (question: string, type: 'old' | 'new', other_params_data?:
const add_answer_text_list = (answer_text_list: Array<any>) => {
answer_text_list.push({ content: '' })
}

const openControl = (event: any) => {
if (props.type !== 'log') {
bus.emit('open-control', event)
}
}

const answer_text_list = computed(() => {
return props.chatRecord.answer_text_list.map((item) => {
if (typeof item == 'string') {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The provided code snippet seems to be related to an interactive component that displays information about an application and provides a way for users to interact with it (by clicking on the card).

Potential Issues and Optimization Suggestions:

  1. Event Handling:

    • There is a @stop directive applied to the .content div, which prevents any further event propagation up to parent elements. This can make sure that clicks within this element don't trigger unnecessary actions outside of its boundary.
  2. Optimization:

    • The use of an inline function inside computed() is generally not recommended because it means the function is recreated whenever anything changes around it. You might consider using an arrow function instead if you want the function to have access to the this context defined in another scope.
  3. Code Duplication:

    • The add_answer_text_list() function duplicates some logic from within answer_text_list(). Consider merging these functions or extracting common functionality into separate utility methods.

Here's the cleaned-up code:

// ... existing imports and props ...

const chatMessage = (question: string, type: 'old' | 'new', other_params_data?: any): void => {
  // Existing implementation ...
}

const addAnswerTextList = (answer_text_list: Array<any>): void => {
  answer_text_list.push({ content: '' });
}

const handleCardClick = (event: any) => {
  if (props.type !== 'log') {
    bus.emit('open-control', event);
  }
};

const answer_texts = computed(() => {
  return props.chatRecord.answer_text_list.map((item) => {
    if (typeof item === 'string') {
      addAnswerTextList(item.split('\n').map(line => ({ content: line })));
      return;
    }
    return item;
  });
});

export default {
  name: 'ApplicationInfo',
  components: {/* already present */ },
  setup(props /* , { emit } as ComputedRef<EmitsResult<MyComputed>> */) {
    // Existing setup logic ...
  }
};

Explanation:

  • handleCardClick: Renamed to clearly indicate what action the handler performs when clicked.
  • merged Functionality: Removed duplicate logic between chatMessage and handleCardClick.

This version maintains readability while improving performance by minimizing redundant code execution.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ import { MsgAlert } from '@/utils/message'
import { type chatType } from '@/api/type/application'
import { useRoute, useRouter } from 'vue-router'
import { getImgUrl } from '@/utils/utils'
import bus from '@/bus'
import 'recorder-core/src/engine/mp3'

import 'recorder-core/src/engine/mp3-engine'
Expand Down Expand Up @@ -542,6 +543,9 @@ function mouseleave() {
}

onMounted(() => {
bus.on('chat-input', (message: string) => {
inputValue.value = message
})
if (question) {
inputValue.value = decodeURIComponent(question.trim())
sendChatHandle()
Expand Down
81 changes: 81 additions & 0 deletions ui/src/components/ai-chat/component/control/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<template>
<div>
<vue3-menus v-model:open="isOpen" :event="eventVal" :menus="menus" hasIcon>
<template #icon="{ menu }"
><AppIcon v-if="menu.icon" :iconName="menu.icon"></AppIcon
></template>
<template #label="{ menu }"> {{ menu.label }}</template>
</vue3-menus>
</div>
</template>
<script setup lang="ts">
import { Vue3Menus } from 'vue3-menus'
import { MsgSuccess } from '@/utils/message'
import AppIcon from '@/components/icons/AppIcon.vue'
import bus from '@/bus'
import { ref, nextTick, onMounted } from 'vue'
const isOpen = ref<boolean>(false)
const eventVal = ref({})
function getSelection() {
const selection = window.getSelection()
if (selection && selection.anchorNode == null) {
return null
}
const text = selection?.anchorNode?.textContent
return text && text.substring(selection.anchorOffset, selection.focusOffset)
}
/**
* 打开控制台
* @param event
*/
const openControl = (event: any) => {
const c = getSelection()
isOpen.value = false
if (c) {
nextTick(() => {
eventVal.value = event
isOpen.value = true
})
event.preventDefault()
}
}

const menus = ref([
{
label: '复制',
icon: 'app-copy',
click: () => {
const selectionText = getSelection()
if (selectionText) {
clearSelectedText()
navigator.clipboard.writeText(selectionText).then(() => {
MsgSuccess('复制成功')
})
}
}
},
{
label: '引用',
icon: 'app-quote',
click: () => {
bus.emit('chat-input', getSelection())
clearSelectedText()
}
}
])
/**
* 清除选中文本
*/
const clearSelectedText = () => {
if (window.getSelection) {
var selection = window.getSelection()
if (selection) {
selection.removeAllRanges()
}
}
}
onMounted(() => {
bus.on('open-control', openControl)
})
</script>
<style lang="scss"></style>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The code looks mostly clean and well-structured, but there are a few things that could be improved:

  1. TypeScript Typing:
    The menus ref is currently of type any, which might lead to runtime errors if not handled properly. Consider defining an interface for your menu data.

  2. Event Emitter:
    The use of eventVal.value = event; isOpen.value = true; after debouncing can be simplified using nextTick.

  3. Code Readability:
    Some comments and spacing could be clearer. For example, adding more spaces around operators or braces can enhance readability.

  4. Clipboard API Check:
    Ensure that the browser supports the Clipboard API before attempting to write to it.

Here's the revised version with these improvements:

@@ -0,0 +1,86 @@
+<template>
+  <div>
+    <vue3-menus v-model:open="isOpen" :event="evenVal" :menus="menus" has-icon> <!-- Changed ':event' to 'v-on:event=' -->
+      <template #icon="{ menu }">
+        <AppIcon v-if="menu.icon" :icon-name="menu.icon"></AppIcon>
+      </template>
+      <template #label="{ menu }">{{ menu.label }}</template>
+    </vue3-menus>
+  </div>
+</template>
+
<script setup lang="ts">
+import { ref, watchEffect, onMounted } from 'vue';
+import Vue3Menus from 'vue3-menus';
+import { MsgSuccess } from '@/utils/message';
+import AppIcon from '@/components/icons/AppIcon.vue';
+import bus from '@/bus';

interface MenuItem {
  label: string;
  icon?: string;
  click?(): void;
}

const isOpen = ref(false);
const evenVal = ref({}); // Corrected ':event' to 'v-on:event='

function getSelection() {
  const selection = window.getSelection();
  if (!selection || !selection.anchorNode) {
    return null;
  }
  const text = selection.ancherNode.textContent;
  return text && text.substring(selection.anchor_offset, selection.focus_offset);
}

/**
* 打开控制台
* @param event
*/
const openControl = async (event: any) => {
  const c = getSelection();
  isOpen.value = false;
  if (c) {
    try {
      await nextTick(); // Wait until DOM is updated
      evenVal.value = event;
      isOpen.value = true;
    } catch (error) {
      console.error('Error opening control:', error);
    }
    event.preventDefault();
  }
};

watchEffect(() => {
  if (window.navigator.clipboard && window.navigator.permissions) {
    bus.on(
      'open-control',
      (evt: Event, selectionText: string | null, clipboardSupported: boolean) => {
        if (clipboardSupported && selectionText !== null) {
          clearSelectedText();
          window.navigator.clipboard.writeText(selectionText).then(
            () => {
              MsgSuccess('复制成功');
            },
            (err) => {
              console.error('Error copying:', err);
            }
          );
        }
      }
    );
  } else {
    bus.off('open-control');
  }
});

function clearSelectedText() {
  if (document.getSelection) {
    let selection = document.getSelection();
    if (selection && selection.type === 'Range') {
      selection.removeAllRanges();
    }
  }
}

onMounted(async () => {
  const clipboardSupported = await window.navigator.permissions.query({ name: "clipboard-write" }).catch(console.log);

  const initialDataForBus = clipboardSupported.state === "granted"
    ? [null, "", clipboardSupported]
    : [];

  bus.emit("initial-data", ...initialDataForBus); // Emit appropriate data based on permission state
});
</script>

<style scoped></style>

Key Changes Made:

  1. Added TypeScript typing for the menus array.
  2. Used await nextTick() to ensure that UI updates are complete before processing user events.
  3. Improved logic to handle Clipboard API permissions and support check.
  4. Clarified comments for better understanding and maintainability of the code.

2 changes: 2 additions & 0 deletions ui/src/components/ai-chat/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
>
<template #operateBefore> <slot name="operateBefore" /> </template>
</ChatInputOperate>
<Control></Control>
</div>
</template>
<script setup lang="ts">
Expand All @@ -63,6 +64,7 @@ import QuestionContent from '@/components/ai-chat/component/question-content/ind
import ChatInputOperate from '@/components/ai-chat/component/chat-input-operate/index.vue'
import PrologueContent from '@/components/ai-chat/component/prologue-content/index.vue'
import UserForm from '@/components/ai-chat/component/user-form/index.vue'
import Control from '@/components/ai-chat/component/control/index.vue'
defineOptions({ name: 'AiChat' })
const route = useRoute()
const {
Expand Down
21 changes: 21 additions & 0 deletions ui/src/components/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1374,4 +1374,25 @@ export const iconMap: any = {
])
}
},
'app-quote': {
iconReader: () => {
return h('i', [
h(
'svg',
{
style: { height: '100%', width: '100%' },
viewBox: '0 0 1024 1024',
version: '1.1',
xmlns: 'http://www.w3.org/2000/svg'
},
[
h('path', {
d: 'M800.768 477.184c-14.336 0-30.72 2.048-45.056 4.096 18.432-51.2 77.824-188.416 237.568-315.392 36.864-28.672-20.48-86.016-59.392-57.344-155.648 116.736-356.352 317.44-356.352 573.44v20.48c0 122.88 100.352 223.232 223.232 223.232S1024 825.344 1024 702.464c0-124.928-100.352-225.28-223.232-225.28zM223.232 477.184c-14.336 0-30.72 2.048-45.056 4.096 18.432-51.2 77.824-188.416 237.568-315.392 36.864-28.672-20.48-86.016-59.392-57.344C200.704 225.28 0 425.984 0 681.984v20.48c0 122.88 100.352 223.232 223.232 223.232s223.232-100.352 223.232-223.232c0-124.928-100.352-225.28-223.232-225.28z',
fill: 'currentColor'
})
]
)
])
}
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The provided code snippet is correct and does not have any significant irregularities or issues. The iconMap object has been updated to include new icons labeled "app-quote".

However, there are a couple of general tips for maintaining clean and optimized code:

  1. Consistent Naming: Use consistent naming conventions throughout the codebase. For example, using either camelCase orkebab-case consistently can make it easier to read and maintain.

  2. Functionality Checks: Before adding new functionality or features, ensure they meet requirements without introducing bugs or inefficiencies.

  3. Documentation: Include comments or documentation if necessary to clarify what each part of the code does.

Overall, this update seems well-intentioned and should function properly with minimal adjustments.

Loading