소스 검색

Feat/attachments (#9526)

Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: JzoNg <jzongcode@gmail.com>
tags/0.10.0
zxhlyh 1 년 전
부모
커밋
7a1d6fe509
No account linked to committer's email address
100개의 변경된 파일1535개의 추가작업 그리고 2832개의 파일을 삭제
  1. 1
    1
      web/app/(commonLayout)/app/(appDetailLayout)/[appId]/annotations/page.tsx
  2. 1
    1
      web/app/(commonLayout)/app/(appDetailLayout)/[appId]/logs/page.tsx
  3. 3
    3
      web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/field.tsx
  4. 9
    2
      web/app/(commonLayout)/apps/Apps.tsx
  5. 1
    1
      web/app/(commonLayout)/datasets/Container.tsx
  6. 3
    3
      web/app/components/app/annotation/empty-element.tsx
  7. 13
    19
      web/app/components/app/annotation/filter.tsx
  8. 3
    3
      web/app/components/app/annotation/index.tsx
  9. 15
    16
      web/app/components/app/annotation/list.tsx
  10. 0
    6
      web/app/components/app/annotation/style.module.css
  11. 86
    0
      web/app/components/app/app-publisher/features-wrapper.tsx
  12. 3
    18
      web/app/components/app/configuration/base/feature-panel/index.tsx
  13. 1
    1
      web/app/components/app/configuration/config-prompt/conversation-history/history-panel.tsx
  14. 5
    2
      web/app/components/app/configuration/config-var/config-modal/field.tsx
  15. 72
    23
      web/app/components/app/configuration/config-var/config-modal/index.tsx
  16. 2
    2
      web/app/components/app/configuration/config-var/config-string/index.tsx
  17. 1
    1
      web/app/components/app/configuration/config-var/index.tsx
  18. 10
    4
      web/app/components/app/configuration/config-var/select-type-item/index.tsx
  19. 0
    40
      web/app/components/app/configuration/config-var/select-type-item/style.module.css
  20. 79
    37
      web/app/components/app/configuration/config-vision/index.tsx
  21. 113
    104
      web/app/components/app/configuration/config-vision/param-config-content.tsx
  22. 3
    3
      web/app/components/app/configuration/config-vision/param-config.tsx
  23. 0
    40
      web/app/components/app/configuration/config-vision/radio-group/index.tsx
  24. 0
    24
      web/app/components/app/configuration/config-vision/radio-group/style.module.css
  25. 0
    220
      web/app/components/app/configuration/config-voice/param-config-content.tsx
  26. 0
    41
      web/app/components/app/configuration/config-voice/param-config.tsx
  27. 1
    1
      web/app/components/app/configuration/config/agent/agent-tools/index.tsx
  28. 20
    6
      web/app/components/app/configuration/config/automatic/get-automatic-res.tsx
  29. 0
    40
      web/app/components/app/configuration/config/feature/add-feature-btn/index.tsx
  30. 0
    52
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/index.tsx
  31. BIN
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/citation.png
  32. 0
    150
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/citation.svg
  33. BIN
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/citations-and-attributions-preview@2x.png
  34. BIN
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/conversation-opener-preview@2x.png
  35. BIN
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/more-like-this-preview@2x.png
  36. BIN
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/more-like-this.png
  37. 0
    188
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/more-like-this.svg
  38. BIN
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/next-question-suggestion-preview@2x.png
  39. BIN
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/opening-statement.png
  40. BIN
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/opening-suggestion-preview@2x.png
  41. BIN
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/speech-to-text-preview@2x.png
  42. BIN
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/speech-to-text.png
  43. 0
    100
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/speech-to-text.svg
  44. BIN
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/suggested-questions-after-answer.png
  45. 0
    163
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/suggested-questions-after-answer.svg
  46. BIN
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/text-to-audio-preview-assistant@2x.png
  47. BIN
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/text-to-audio-preview-completion@2x.png
  48. 0
    41
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/style.module.css
  49. 0
    172
      web/app/components/app/configuration/config/feature/choose-feature/index.tsx
  50. 0
    31
      web/app/components/app/configuration/config/feature/feature-group/index.tsx
  51. 2
    229
      web/app/components/app/configuration/config/index.tsx
  52. 1
    1
      web/app/components/app/configuration/dataset-config/index.tsx
  53. 6
    4
      web/app/components/app/configuration/dataset-config/settings-modal/index.tsx
  54. 109
    0
      web/app/components/app/configuration/debug/chat-user-input.tsx
  55. 26
    6
      web/app/components/app/configuration/debug/debug-with-multiple-model/chat-item.tsx
  56. 28
    17
      web/app/components/app/configuration/debug/debug-with-multiple-model/index.tsx
  57. 10
    15
      web/app/components/app/configuration/debug/debug-with-multiple-model/text-generation-item.tsx
  58. 37
    8
      web/app/components/app/configuration/debug/debug-with-single-model/index.tsx
  59. 119
    86
      web/app/components/app/configuration/debug/index.tsx
  60. 0
    25
      web/app/components/app/configuration/features/chat-group/citation/index.tsx
  61. 0
    65
      web/app/components/app/configuration/features/chat-group/index.tsx
  62. 0
    25
      web/app/components/app/configuration/features/chat-group/speech-to-text/index.tsx
  63. 0
    34
      web/app/components/app/configuration/features/chat-group/suggested-questions-after-answer/index.tsx
  64. 0
    55
      web/app/components/app/configuration/features/chat-group/text-to-speech/index.tsx
  65. 229
    174
      web/app/components/app/configuration/index.tsx
  66. 126
    164
      web/app/components/app/configuration/prompt-value-panel/index.tsx
  67. 13
    0
      web/app/components/app/configuration/prompt-value-panel/utils.ts
  68. 0
    80
      web/app/components/app/configuration/toolbox/moderation/index.tsx
  69. 1
    1
      web/app/components/app/configuration/tools/external-data-tool-modal.tsx
  70. 6
    4
      web/app/components/app/create-app-modal/index.tsx
  71. 2
    2
      web/app/components/app/create-from-dsl-modal/index.tsx
  72. 3
    2
      web/app/components/app/duplicate-modal/index.tsx
  73. 2
    2
      web/app/components/app/log-annotation/index.tsx
  74. 38
    40
      web/app/components/app/log/filter.tsx
  75. 5
    5
      web/app/components/app/log/index.tsx
  76. 38
    30
      web/app/components/app/log/list.tsx
  77. 0
    6
      web/app/components/app/log/style.module.css
  78. 19
    9
      web/app/components/app/overview/settings/index.tsx
  79. 0
    5
      web/app/components/app/overview/settings/style.module.css
  80. 4
    0
      web/app/components/app/store.ts
  81. 6
    4
      web/app/components/app/switch-app-modal/index.tsx
  82. 2
    2
      web/app/components/app/text-generate/item/index.tsx
  83. 21
    10
      web/app/components/app/text-generate/item/result-tab.tsx
  84. 3
    3
      web/app/components/app/workflow-log/detail.tsx
  85. 26
    38
      web/app/components/app/workflow-log/filter.tsx
  86. 6
    6
      web/app/components/app/workflow-log/index.tsx
  87. 34
    30
      web/app/components/app/workflow-log/list.tsx
  88. 0
    6
      web/app/components/app/workflow-log/style.module.css
  89. 2
    2
      web/app/components/base/button/add-button.tsx
  90. 27
    33
      web/app/components/base/chat/chat-with-history/chat-wrapper.tsx
  91. 3
    2
      web/app/components/base/chat/chat-with-history/config-panel/form-input.tsx
  92. 34
    4
      web/app/components/base/chat/chat-with-history/config-panel/form.tsx
  93. 2
    0
      web/app/components/base/chat/chat-with-history/context.tsx
  94. 48
    14
      web/app/components/base/chat/chat-with-history/hooks.tsx
  95. 2
    0
      web/app/components/base/chat/chat-with-history/index.tsx
  96. 13
    16
      web/app/components/base/chat/chat/answer/agent-content.tsx
  97. 11
    2
      web/app/components/base/chat/chat/answer/basic-content.tsx
  98. 25
    6
      web/app/components/base/chat/chat/answer/index.tsx
  99. 1
    1
      web/app/components/base/chat/chat/answer/operation.tsx
  100. 0
    0
      web/app/components/base/chat/chat/answer/tool-detail.tsx

+ 1
- 1
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/annotations/page.tsx 파일 보기

import React from 'react' import React from 'react'
import Main from '@/app/components/app/log-annotation' import Main from '@/app/components/app/log-annotation'
import { PageType } from '@/app/components/app/configuration/toolbox/annotation/type'
import { PageType } from '@/app/components/base/features/new-feature-panel/annotation-reply/type'


export type IProps = { export type IProps = {
params: { appId: string } params: { appId: string }

+ 1
- 1
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/logs/page.tsx 파일 보기

import React from 'react' import React from 'react'
import Main from '@/app/components/app/log-annotation' import Main from '@/app/components/app/log-annotation'
import { PageType } from '@/app/components/app/configuration/toolbox/annotation/type'
import { PageType } from '@/app/components/base/features/new-feature-panel/annotation-reply/type'


const Logs = async () => { const Logs = async () => {
return ( return (

+ 3
- 3
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/field.tsx 파일 보기

import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React from 'react'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import Input from '@/app/components/base/input'


type Props = { type Props = {
className?: string className?: string
<div className={cn(labelClassName, 'flex items-center h-[18px] text-[13px] font-medium text-gray-900')}>{label} </div> <div className={cn(labelClassName, 'flex items-center h-[18px] text-[13px] font-medium text-gray-900')}>{label} </div>
{isRequired && <span className='ml-0.5 text-xs font-semibold text-[#D92D20]'>*</span>} {isRequired && <span className='ml-0.5 text-xs font-semibold text-[#D92D20]'>*</span>}
</div> </div>
<input
type='text'
<Input
value={value} value={value}
onChange={e => onChange(e.target.value)} onChange={e => onChange(e.target.value)}
className='flex h-9 w-full py-1 px-2 rounded-lg text-xs leading-normal bg-gray-100 caret-primary-600 hover:bg-gray-100 focus:ring-1 focus:ring-inset focus:ring-gray-200 focus-visible:outline-none focus:bg-gray-50 placeholder:text-gray-400'
className='h-9'
placeholder={placeholder} placeholder={placeholder}
/> />
</div> </div>

+ 9
- 2
web/app/(commonLayout)/apps/Apps.tsx 파일 보기

import { CheckModal } from '@/hooks/use-pay' import { CheckModal } from '@/hooks/use-pay'
import TabSliderNew from '@/app/components/base/tab-slider-new' import TabSliderNew from '@/app/components/base/tab-slider-new'
import { useTabSearchParams } from '@/hooks/use-tab-searchparams' import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
import SearchInput from '@/app/components/base/search-input'
import Input from '@/app/components/base/input'
import { useStore as useTagStore } from '@/app/components/base/tag-management/store' import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
import TagManagementModal from '@/app/components/base/tag-management' import TagManagementModal from '@/app/components/base/tag-management'
import TagFilter from '@/app/components/base/tag-management/filter' import TagFilter from '@/app/components/base/tag-management/filter'
/> />
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<TagFilter type='app' value={tagFilterValue} onChange={handleTagsChange} /> <TagFilter type='app' value={tagFilterValue} onChange={handleTagsChange} />
<SearchInput className='w-[200px]' value={keywords} onChange={handleKeywordsChange} />
<Input
showLeftIcon
showClearIcon
wrapperClassName='w-[200px]'
value={keywords}
onChange={e => handleKeywordsChange(e.target.value)}
onClear={() => handleKeywordsChange('')}
/>
</div> </div>
</div> </div>
<nav className='grid content-start grid-cols-1 gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'> <nav className='grid content-start grid-cols-1 gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'>

+ 1
- 1
web/app/(commonLayout)/datasets/Container.tsx 파일 보기

import ApiServer from './ApiServer' import ApiServer from './ApiServer'
import Doc from './Doc' import Doc from './Doc'
import TabSliderNew from '@/app/components/base/tab-slider-new' import TabSliderNew from '@/app/components/base/tab-slider-new'
import SearchInput from '@/app/components/base/search-input'
import TagManagementModal from '@/app/components/base/tag-management' import TagManagementModal from '@/app/components/base/tag-management'
import TagFilter from '@/app/components/base/tag-management/filter' import TagFilter from '@/app/components/base/tag-management/filter'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development' import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
import SearchInput from '@/app/components/base/search-input'


// Services // Services
import { fetchDatasetApiBaseUrl } from '@/service/datasets' import { fetchDatasetApiBaseUrl } from '@/service/datasets'

+ 3
- 3
web/app/components/app/annotation/empty-element.tsx 파일 보기



return ( return (
<div className='flex items-center justify-center h-full'> <div className='flex items-center justify-center h-full'>
<div className='bg-gray-50 w-[560px] h-fit box-border px-5 py-4 rounded-2xl'>
<span className='text-gray-700 font-semibold'>{t('appAnnotation.noData.title')}<ThreeDotsIcon className='inline relative -top-3 -left-1.5' /></span>
<div className='mt-2 text-gray-500 text-sm font-normal'>
<div className='bg-background-section-burn w-[560px] h-fit box-border px-5 py-4 rounded-2xl'>
<span className='text-text-secondary system-md-semibold'>{t('appAnnotation.noData.title')}<ThreeDotsIcon className='inline relative -top-3 -left-1.5' /></span>
<div className='mt-2 text-text-tertiary system-sm-regular'>
{t('appAnnotation.noData.description')} {t('appAnnotation.noData.description')}
</div> </div>
</div> </div>

+ 13
- 19
web/app/components/app/annotation/filter.tsx 파일 보기

import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import {
MagnifyingGlassIcon,
} from '@heroicons/react/24/solid'
import useSWR from 'swr' import useSWR from 'swr'
import Input from '@/app/components/base/input'
import { fetchAnnotationsCount } from '@/service/log' import { fetchAnnotationsCount } from '@/service/log'


export type QueryParam = { export type QueryParam = {
if (!data) if (!data)
return null return null
return ( return (
<div className='flex justify-between flex-row flex-wrap gap-y-2 gap-x-4 items-center mb-4 text-gray-900 text-base'>
<div className="relative">
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<MagnifyingGlassIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div>
<input
type="text"
name="query"
className="block w-[240px] bg-gray-100 shadow-sm rounded-md border-0 py-1.5 pl-10 text-gray-900 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-gray-200 focus-visible:outline-none sm:text-sm sm:leading-6"
placeholder={t('common.operation.search') as string}
value={queryParams.keyword}
onChange={(e) => {
setQueryParams({ ...queryParams, keyword: e.target.value })
}}
/>
</div>
<div className='flex justify-between flex-row flex-wrap gap-2 items-center mb-2'>
<Input
wrapperClassName='w-[200px]'
showLeftIcon
showClearIcon
value={queryParams.keyword}
placeholder={t('common.operation.search')!}
onChange={(e) => {
setQueryParams({ ...queryParams, keyword: e.target.value })
}}
onClear={() => setQueryParams({ ...queryParams, keyword: '' })}
/>
{children} {children}
</div> </div>
) )

+ 3
- 3
web/app/components/app/annotation/index.tsx 파일 보기

import { addAnnotation, delAnnotation, fetchAnnotationConfig as doFetchAnnotationConfig, editAnnotation, fetchAnnotationList, queryAnnotationJobStatus, updateAnnotationScore, updateAnnotationStatus } from '@/service/annotation' import { addAnnotation, delAnnotation, fetchAnnotationConfig as doFetchAnnotationConfig, editAnnotation, fetchAnnotationList, queryAnnotationJobStatus, updateAnnotationScore, updateAnnotationStatus } from '@/service/annotation'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import { APP_PAGE_LIMIT } from '@/config' import { APP_PAGE_LIMIT } from '@/config'
import ConfigParamModal from '@/app/components/app/configuration/toolbox/annotation/config-param-modal'
import ConfigParamModal from '@/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal'
import type { AnnotationReplyConfig } from '@/models/debug' import type { AnnotationReplyConfig } from '@/models/debug'
import { sleep } from '@/utils' import { sleep } from '@/utils'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'


return ( return (
<div className='flex flex-col h-full'> <div className='flex flex-col h-full'>
<p className='flex text-sm font-normal text-gray-500'>{t('appLog.description')}</p>
<div className='grow flex flex-col py-4 '>
<p className='text-text-tertiary system-sm-regular'>{t('appLog.description')}</p>
<div className='flex flex-col py-4 flex-1'>
<Filter appId={appDetail.id} queryParams={queryParams} setQueryParams={setQueryParams}> <Filter appId={appDetail.id} queryParams={queryParams} setQueryParams={setQueryParams}>
<div className='flex items-center space-x-2'> <div className='flex items-center space-x-2'>
{isChatApp && ( {isChatApp && (

+ 15
- 16
web/app/components/app/annotation/list.tsx 파일 보기

import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { RiDeleteBinLine } from '@remixicon/react' import { RiDeleteBinLine } from '@remixicon/react'
import { Edit02 } from '../../base/icons/src/vender/line/general' import { Edit02 } from '../../base/icons/src/vender/line/general'
import s from './style.module.css'
import type { AnnotationItem } from './type' import type { AnnotationItem } from './type'
import RemoveAnnotationConfirmModal from './remove-annotation-confirm-modal' import RemoveAnnotationConfirmModal from './remove-annotation-confirm-modal'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
const [showConfirmDelete, setShowConfirmDelete] = React.useState(false) const [showConfirmDelete, setShowConfirmDelete] = React.useState(false)
return ( return (
<div className='overflow-x-auto'> <div className='overflow-x-auto'>
<table className={cn(s.logTable, 'w-full min-w-[440px] border-collapse border-0 text-sm')} >
<thead className="h-8 leading-8 border-b border-gray-200 text-gray-500 font-bold">
<tr className='uppercase'>
<td className='whitespace-nowrap'>{t('appAnnotation.table.header.question')}</td>
<td className='whitespace-nowrap'>{t('appAnnotation.table.header.answer')}</td>
<td className='whitespace-nowrap'>{t('appAnnotation.table.header.createdAt')}</td>
<td className='whitespace-nowrap'>{t('appAnnotation.table.header.hits')}</td>
<td className='whitespace-nowrap w-[96px]'>{t('appAnnotation.table.header.actions')}</td>
<table className={cn('mt-2 w-full min-w-[440px] border-collapse border-0')}>
<thead className='system-xs-medium-uppercase text-text-tertiary'>
<tr>
<td className='pl-2 pr-1 w-5 rounded-l-lg bg-background-section-burn whitespace-nowrap'>{t('appAnnotation.table.header.question')}</td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appAnnotation.table.header.answer')}</td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appAnnotation.table.header.createdAt')}</td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appAnnotation.table.header.hits')}</td>
<td className='pl-3 py-1.5 rounded-r-lg bg-background-section-burn whitespace-nowrap w-[96px]'>{t('appAnnotation.table.header.actions')}</td>
</tr> </tr>
</thead> </thead>
<tbody className="text-gray-500">
<tbody className="text-text-secondary system-sm-regular">
{list.map(item => ( {list.map(item => (
<tr <tr
key={item.id} key={item.id}
className={'border-b border-gray-200 h-8 hover:bg-gray-50 cursor-pointer'}
className='border-b border-divider-subtle hover:bg-background-default-hover cursor-pointer'
onClick={ onClick={
() => { () => {
onView(item) onView(item)
} }
> >
<td <td
className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[250px]'
className='p-3 pr-2 whitespace-nowrap overflow-hidden text-ellipsis max-w-[250px]'
title={item.question} title={item.question}
>{item.question}</td> >{item.question}</td>
<td <td
className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[250px]'
className='p-3 pr-2 whitespace-nowrap overflow-hidden text-ellipsis max-w-[250px]'
title={item.answer} title={item.answer}
>{item.answer}</td> >{item.answer}</td>
<td>{formatTime(item.created_at, t('appLog.dateTimeFormat') as string)}</td>
<td>{item.hit_count}</td>
<td className='w-[96px]' onClick={e => e.stopPropagation()}>
<td className='p-3 pr-2'>{formatTime(item.created_at, t('appLog.dateTimeFormat') as string)}</td>
<td className='p-3 pr-2'>{item.hit_count}</td>
<td className='w-[96px] p-3 pr-2' onClick={e => e.stopPropagation()}>
{/* Actions */} {/* Actions */}
<div className='flex space-x-2 text-gray-500'> <div className='flex space-x-2 text-gray-500'>
<div <div

+ 0
- 6
web/app/components/app/annotation/style.module.css 파일 보기

.logTable td {
padding: 7px 8px;
box-sizing: border-box;
max-width: 200px;
}

.pagination li { .pagination li {
list-style: none; list-style: none;
} }

+ 86
- 0
web/app/components/app/app-publisher/features-wrapper.tsx 파일 보기

import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import produce from 'immer'
import type { AppPublisherProps } from '@/app/components/app/app-publisher'
import Confirm from '@/app/components/base/confirm'
import AppPublisher from '@/app/components/app/app-publisher'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
import type { ModelAndParameter } from '@/app/components/app/configuration/debug/types'
import type { FileUpload } from '@/app/components/base/features/types'
import { Resolution } from '@/types/app'
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
import { SupportUploadFileTypes } from '@/app/components/workflow/types'

type Props = Omit<AppPublisherProps, 'onPublish'> & {
onPublish?: (modelAndParameter?: ModelAndParameter, features?: any) => Promise<any> | any
publishedConfig?: any
resetAppConfig?: () => void
}

const FeaturesWrappedAppPublisher = (props: Props) => {
const { t } = useTranslation()
const features = useFeatures(s => s.features)
const featuresStore = useFeaturesStore()
const [restoreConfirmOpen, setRestoreConfirmOpen] = useState(false)
const handleConfirm = useCallback(() => {
props.resetAppConfig?.()
const {
features,
setFeatures,
} = featuresStore!.getState()
const newFeatures = produce(features, (draft) => {
draft.moreLikeThis = props.publishedConfig.modelConfig.more_like_this || { enabled: false }
draft.opening = {
enabled: !!props.publishedConfig.modelConfig.opening_statement,
opening_statement: props.publishedConfig.modelConfig.opening_statement || '',
suggested_questions: props.publishedConfig.modelConfig.suggested_questions || [],
}
draft.moderation = props.publishedConfig.modelConfig.sensitive_word_avoidance || { enabled: false }
draft.speech2text = props.publishedConfig.modelConfig.speech_to_text || { enabled: false }
draft.text2speech = props.publishedConfig.modelConfig.text_to_speech || { enabled: false }
draft.suggested = props.publishedConfig.modelConfig.suggested_questions_after_answer || { enabled: false }
draft.citation = props.publishedConfig.modelConfig.retriever_resource || { enabled: false }
draft.annotationReply = props.publishedConfig.modelConfig.annotation_reply || { enabled: false }
draft.file = {
image: {
detail: props.publishedConfig.modelConfig.file_upload?.image?.detail || Resolution.high,
enabled: !!props.publishedConfig.modelConfig.file_upload?.image?.enabled,
number_limits: props.publishedConfig.modelConfig.file_upload?.image?.number_limits || 3,
transfer_methods: props.publishedConfig.modelConfig.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
},
enabled: !!(props.publishedConfig.modelConfig.file_upload?.enabled || props.publishedConfig.modelConfig.file_upload?.image?.enabled),
allowed_file_types: props.publishedConfig.modelConfig.file_upload?.allowed_file_types || [SupportUploadFileTypes.image],
allowed_file_extensions: props.publishedConfig.modelConfig.file_upload?.allowed_file_extensions || FILE_EXTS[SupportUploadFileTypes.image].map(ext => `.${ext}`),
allowed_file_upload_methods: props.publishedConfig.modelConfig.file_upload?.allowed_file_upload_methods || props.publishedConfig.modelConfig.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
number_limits: props.publishedConfig.modelConfig.file_upload?.number_limits || props.publishedConfig.modelConfig.file_upload?.image?.number_limits || 3,
} as FileUpload
})
setFeatures(newFeatures)
setRestoreConfirmOpen(false)
}, [featuresStore, props])

const handlePublish = useCallback((modelAndParameter?: ModelAndParameter) => {
return props.onPublish?.(modelAndParameter, features)
}, [features, props])

return (
<>
<AppPublisher {...{
...props,
onPublish: handlePublish,
onRestore: () => setRestoreConfirmOpen(true),
}}/>
{restoreConfirmOpen && (
<Confirm
title={t('appDebug.resetConfig.title')}
content={t('appDebug.resetConfig.message')}
isShow={restoreConfirmOpen}
onConfirm={handleConfirm}
onCancel={() => setRestoreConfirmOpen(false)}
/>
)}
</>
)
}

export default FeaturesWrappedAppPublisher

+ 3
- 18
web/app/components/app/configuration/base/feature-panel/index.tsx 파일 보기

import type { FC, ReactNode } from 'react' import type { FC, ReactNode } from 'react'
import React from 'react' import React from 'react'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import ParamsConfig from '@/app/components/app/configuration/config-voice/param-config'


export type IFeaturePanelProps = { export type IFeaturePanelProps = {
className?: string className?: string
title: ReactNode title: ReactNode
headerRight?: ReactNode headerRight?: ReactNode
hasHeaderBottomBorder?: boolean hasHeaderBottomBorder?: boolean
isFocus?: boolean
noBodySpacing?: boolean noBodySpacing?: boolean
children?: ReactNode children?: ReactNode
isShowTextToSpeech?: boolean
} }


const FeaturePanel: FC<IFeaturePanelProps> = ({ const FeaturePanel: FC<IFeaturePanelProps> = ({
title, title,
headerRight, headerRight,
hasHeaderBottomBorder, hasHeaderBottomBorder,
isFocus,
noBodySpacing, noBodySpacing,
children, children,
isShowTextToSpeech,
}) => { }) => {
return ( return (
<div
className={cn(className, isFocus && 'border border-[#2D0DEE]', 'rounded-xl bg-gray-50 pt-2 pb-3', noBodySpacing && '!pb-0')}
style={isFocus
? {
boxShadow: '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)',
}
: {}}
>
<div className={cn('rounded-xl border-t-[0.5px] border-l-[0.5px] bg-background-section-burn pb-3', noBodySpacing && '!pb-0', className)}>
{/* Header */} {/* Header */}
<div className={cn('pb-2 px-3', hasHeaderBottomBorder && 'border-b border-gray-100')}>
<div className={cn('px-3 pt-2', hasHeaderBottomBorder && 'border-b border-divider-subtle')}>
<div className='flex justify-between items-center h-8'> <div className='flex justify-between items-center h-8'>
<div className='flex items-center space-x-1 shrink-0'> <div className='flex items-center space-x-1 shrink-0'>
{headerIcon && <div className='flex items-center justify-center w-6 h-6'>{headerIcon}</div>} {headerIcon && <div className='flex items-center justify-center w-6 h-6'>{headerIcon}</div>}
<div className='text-sm font-semibold text-gray-800'>{title}</div>
<div className='text-text-secondary system-sm-semibold'>{title}</div>
</div> </div>
<div className='flex gap-2 items-center'> <div className='flex gap-2 items-center'>
{headerRight && <div>{headerRight}</div>} {headerRight && <div>{headerRight}</div>}
{isShowTextToSpeech && <div className='flex items-center'>
<ParamsConfig />
</div>}
</div> </div>
</div> </div>
</div> </div>

+ 1
- 1
web/app/components/app/configuration/config-prompt/conversation-history/history-panel.tsx 파일 보기



return ( return (
<Panel <Panel
className='mt-3'
className='mt-2'
title={ title={
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<div>{t('appDebug.feature.conversationHistory.title')}</div> <div>{t('appDebug.feature.conversationHistory.title')}</div>

+ 5
- 2
web/app/components/app/configuration/config-var/config-modal/field.tsx 파일 보기

'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React from 'react'
import cn from '@/utils/classnames'


type Props = { type Props = {
className?: string
title: string title: string
children: JSX.Element children: JSX.Element
} }


const Field: FC<Props> = ({ const Field: FC<Props> = ({
className,
title, title,
children, children,
}) => { }) => {
return ( return (
<div>
<div className='leading-8 text-[13px] font-medium text-gray-700'>{title}</div>
<div className={cn(className)}>
<div className='text-text-secondary system-sm-semibold leading-8'>{title}</div>
<div>{children}</div> <div>{children}</div>
</div> </div>
) )

+ 72
- 23
web/app/components/app/configuration/config-var/config-modal/index.tsx 파일 보기

import React, { useCallback, useEffect, useRef, useState } from 'react' import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import produce from 'immer'
import ModalFoot from '../modal-foot' import ModalFoot from '../modal-foot'
import ConfigSelect from '../config-select' import ConfigSelect from '../config-select'
import ConfigString from '../config-string' import ConfigString from '../config-string'
import SelectTypeItem from '../select-type-item' import SelectTypeItem from '../select-type-item'
import Field from './field' import Field from './field'
import Input from '@/app/components/base/input'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import { checkKeys, getNewVarInWorkflow } from '@/utils/var' import { checkKeys, getNewVarInWorkflow } from '@/utils/var'
import ConfigContext from '@/context/debug-configuration' import ConfigContext from '@/context/debug-configuration'
import type { InputVar, MoreInfo } from '@/app/components/workflow/types'
import type { InputVar, MoreInfo, UploadFileSetting } from '@/app/components/workflow/types'
import Modal from '@/app/components/base/modal' import Modal from '@/app/components/base/modal'
import Switch from '@/app/components/base/switch'
import { ChangeType, InputVarType } from '@/app/components/workflow/types'
import { ChangeType, InputVarType, SupportUploadFileTypes } from '@/app/components/workflow/types'
import FileUploadSetting from '@/app/components/workflow/nodes/_base/components/file-upload-setting'
import Checkbox from '@/app/components/base/checkbox'
import { DEFAULT_FILE_UPLOAD_SETTING } from '@/app/components/workflow/constants'
import { DEFAULT_VALUE_MAX_LEN } from '@/config'


const TEXT_MAX_LENGTH = 256 const TEXT_MAX_LENGTH = 256


varKeys?: string[] varKeys?: string[]
onClose: () => void onClose: () => void
onConfirm: (newValue: InputVar, moreInfo?: MoreInfo) => void onConfirm: (newValue: InputVar, moreInfo?: MoreInfo) => void
supportFile?: boolean
} }


const inputClassName = 'w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-100 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200'

const ConfigModal: FC<IConfigModalProps> = ({ const ConfigModal: FC<IConfigModalProps> = ({
isCreate, isCreate,
payload, payload,
isShow, isShow,
onClose, onClose,
onConfirm, onConfirm,
supportFile,
}) => { }) => {
const { modelConfig } = useContext(ConfigContext) const { modelConfig } = useContext(ConfigContext)
const { t } = useTranslation() const { t } = useTranslation()
}, [isShow]) }, [isShow])


const isStringInput = type === InputVarType.textInput || type === InputVarType.paragraph const isStringInput = type === InputVarType.textInput || type === InputVarType.paragraph
const checkVariableName = useCallback((value: string) => {
const { isValid, errorMessageKey } = checkKeys([value], false)
const checkVariableName = useCallback((value: string, canBeEmpty?: boolean) => {
const { isValid, errorMessageKey } = checkKeys([value], canBeEmpty)
if (!isValid) { if (!isValid) {
Toast.notify({ Toast.notify({
type: 'error', type: 'error',
} }
}, []) }, [])


const handleTypeChange = useCallback((type: InputVarType) => {
return () => {
const newPayload = produce(tempPayload, (draft) => {
draft.type = type
if ([InputVarType.singleFile, InputVarType.multiFiles].includes(type)) {
(Object.keys(DEFAULT_FILE_UPLOAD_SETTING)).forEach((key) => {
if (key !== 'max_length')
(draft as any)[key] = (DEFAULT_FILE_UPLOAD_SETTING as any)[key]
})
if (type === InputVarType.multiFiles)
draft.max_length = DEFAULT_FILE_UPLOAD_SETTING.max_length
}
if (type === InputVarType.paragraph)
draft.max_length = DEFAULT_VALUE_MAX_LEN
})
setTempPayload(newPayload)
}
}, [tempPayload])

const handleVarKeyBlur = useCallback((e: any) => { const handleVarKeyBlur = useCallback((e: any) => {
const varName = e.target.value const varName = e.target.value
if (!checkVariableName(varName) || tempPayload.label)
if (!checkVariableName(varName, true) || tempPayload.label)
return return


setTempPayload((prev) => { setTempPayload((prev) => {
if (isStringInput || type === InputVarType.number) { if (isStringInput || type === InputVarType.number) {
onConfirm(tempPayload, moreInfo) onConfirm(tempPayload, moreInfo)
} }
else {
else if (type === InputVarType.select) {
if (options?.length === 0) { if (options?.length === 0) {
Toast.notify({ type: 'error', message: t('appDebug.variableConfig.errorMsg.atLeastOneOption') }) Toast.notify({ type: 'error', message: t('appDebug.variableConfig.errorMsg.atLeastOneOption') })
return return
} }
onConfirm(tempPayload, moreInfo) onConfirm(tempPayload, moreInfo)
} }
else if ([InputVarType.singleFile, InputVarType.multiFiles].includes(type)) {
if (tempPayload.allowed_file_types?.length === 0) {
const errorMessages = t('workflow.errorMsg.fieldRequired', { field: t('appDebug.variableConfig.file.supportFileTypes') })
Toast.notify({ type: 'error', message: errorMessages })
return
}
if (tempPayload.allowed_file_types?.includes(SupportUploadFileTypes.custom) && !tempPayload.allowed_file_extensions?.length) {
const errorMessages = t('workflow.errorMsg.fieldRequired', { field: t('appDebug.variableConfig.file.custom.name') })
Toast.notify({ type: 'error', message: errorMessages })
return
}
onConfirm(tempPayload, moreInfo)
}
else {
onConfirm(tempPayload, moreInfo)
}
} }


return ( return (
<div className='space-y-2'> <div className='space-y-2'>


<Field title={t('appDebug.variableConfig.fieldType')}> <Field title={t('appDebug.variableConfig.fieldType')}>
<div className='flex space-x-2'>
<SelectTypeItem type={InputVarType.textInput} selected={type === InputVarType.textInput} onClick={() => handlePayloadChange('type')(InputVarType.textInput)} />
<SelectTypeItem type={InputVarType.paragraph} selected={type === InputVarType.paragraph} onClick={() => handlePayloadChange('type')(InputVarType.paragraph)} />
<SelectTypeItem type={InputVarType.select} selected={type === InputVarType.select} onClick={() => handlePayloadChange('type')(InputVarType.select)} />
<SelectTypeItem type={InputVarType.number} selected={type === InputVarType.number} onClick={() => handlePayloadChange('type')(InputVarType.number)} />
<div className='grid grid-cols-3 gap-2'>
<SelectTypeItem type={InputVarType.textInput} selected={type === InputVarType.textInput} onClick={handleTypeChange(InputVarType.textInput)} />
<SelectTypeItem type={InputVarType.paragraph} selected={type === InputVarType.paragraph} onClick={handleTypeChange(InputVarType.paragraph)} />
<SelectTypeItem type={InputVarType.select} selected={type === InputVarType.select} onClick={handleTypeChange(InputVarType.select)} />
<SelectTypeItem type={InputVarType.number} selected={type === InputVarType.number} onClick={handleTypeChange(InputVarType.number)} />
{supportFile && <>
<SelectTypeItem type={InputVarType.singleFile} selected={type === InputVarType.singleFile} onClick={handleTypeChange(InputVarType.singleFile)} />
<SelectTypeItem type={InputVarType.multiFiles} selected={type === InputVarType.multiFiles} onClick={handleTypeChange(InputVarType.multiFiles)} />
</>}
</div> </div>
</Field> </Field>


<Field title={t('appDebug.variableConfig.varName')}> <Field title={t('appDebug.variableConfig.varName')}>
<input
type='text'
className={inputClassName}
<Input
value={variable} value={variable}
onChange={e => handlePayloadChange('variable')(e.target.value)} onChange={e => handlePayloadChange('variable')(e.target.value)}
onBlur={handleVarKeyBlur} onBlur={handleVarKeyBlur}
/> />
</Field> </Field>
<Field title={t('appDebug.variableConfig.labelName')}> <Field title={t('appDebug.variableConfig.labelName')}>
<input
type='text'
className={inputClassName}
<Input
value={label as string} value={label as string}
onChange={e => handlePayloadChange('label')(e.target.value)} onChange={e => handlePayloadChange('label')(e.target.value)}
placeholder={t('appDebug.variableConfig.inputPlaceholder')!} placeholder={t('appDebug.variableConfig.inputPlaceholder')!}
</Field> </Field>
)} )}


<Field title={t('appDebug.variableConfig.required')}>
<Switch defaultValue={tempPayload.required} onChange={handlePayloadChange('required')} />
</Field>
{[InputVarType.singleFile, InputVarType.multiFiles].includes(type) && (
<FileUploadSetting
payload={tempPayload as UploadFileSetting}
onChange={(p: UploadFileSetting) => setTempPayload(p as InputVar)}
isMultiple={type === InputVarType.multiFiles}
/>
)}

<div className='!mt-5 flex items-center h-6 space-x-2'>
<Checkbox checked={tempPayload.required} onCheck={() => handlePayloadChange('required')(!tempPayload.required)} />
<span className='text-text-secondary system-sm-semibold'>{t('appDebug.variableConfig.required')}</span>
</div>
</div> </div>
</div> </div>
<ModalFoot <ModalFoot

+ 2
- 2
web/app/components/app/configuration/config-var/config-string/index.tsx 파일 보기

'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import Input from '@/app/components/base/input'


export type IConfigStringProps = { export type IConfigStringProps = {
value: number | undefined value: number | undefined


return ( return (
<div> <div>
<input
<Input
type="number" type="number"
max={maxLength} max={maxLength}
min={1} min={1}


onChange(value) onChange(value)
}} }}
className="w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-100 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
/> />
</div> </div>
) )

+ 1
- 1
web/app/components/app/configuration/config-var/index.tsx 파일 보기

} }
return ( return (
<Panel <Panel
className="mt-4"
className="mt-2"
headerIcon={ headerIcon={
<VarIcon className='w-4 h-4 text-primary-500' /> <VarIcon className='w-4 h-4 text-primary-500' />
} }

+ 10
- 4
web/app/components/app/configuration/config-var/select-type-item/index.tsx 파일 보기

import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import s from './style.module.css'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import type { InputVarType } from '@/app/components/workflow/types' import type { InputVarType } from '@/app/components/workflow/types'
import InputVarTypeIcon from '@/app/components/workflow/nodes/_base/components/input-var-type-icon' import InputVarTypeIcon from '@/app/components/workflow/nodes/_base/components/input-var-type-icon'
onClick: () => void onClick: () => void
} }


const i18nFileTypeMap: Record<string, string> = {
'file': 'single-file',
'file-list': 'multi-files',
}

const SelectTypeItem: FC<ISelectTypeItemProps> = ({ const SelectTypeItem: FC<ISelectTypeItemProps> = ({
type, type,
selected, selected,
onClick, onClick,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const typeName = t(`appDebug.variableConfig.${type}`)
const typeName = t(`appDebug.variableConfig.${i18nFileTypeMap[type] || type}`)


return ( return (
<div <div
className={cn(s.item, selected && s.selected, 'space-y-1')}
className={cn(
'flex flex-col justify-center items-center h-[58px] rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg space-y-1',
selected ? 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-xs system-xs-medium' : ' hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs cursor-pointer system-xs-regular')}
onClick={onClick} onClick={onClick}
> >
<div className='shrink-0'> <div className='shrink-0'>
<InputVarTypeIcon type={type} className='w-5 h-5' /> <InputVarTypeIcon type={type} className='w-5 h-5' />
</div> </div>
<span className={cn(s.text)}>{typeName}</span>
<span>{typeName}</span>
</div> </div>
) )
} }

+ 0
- 40
web/app/components/app/configuration/config-var/select-type-item/style.module.css 파일 보기

.item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 58px;
width: 98px;
border-radius: 8px;
border: 1px solid #EAECF0;
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
background-color: #fff;
cursor: pointer;
}

.item:not(.selected):hover {
border-color: #B2CCFF;
background-color: #F5F8FF;
box-shadow: 0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06);
}

.item.selected {
color: #155EEF;
border-color: #528BFF;
background-color: #F5F8FF;
box-shadow: 0px 1px 3px rgba(16, 24, 40, 0.1), 0px 1px 2px rgba(16, 24, 40, 0.06);
}

.text {
font-size: 13px;
color: #667085;
font-weight: 500;
}

.item.selected .text {
color: #155EEF;
}

.item:not(.selected):hover {
color: #344054;
}

+ 79
- 37
web/app/components/app/configuration/config-vision/index.tsx 파일 보기

'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import produce from 'immer'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import Panel from '../base/feature-panel'
import ParamConfig from './param-config' import ParamConfig from './param-config'
import { Vision } from '@/app/components/base/icons/src/vender/features'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import Switch from '@/app/components/base/switch'
import { Eye } from '@/app/components/base/icons/src/vender/solid/general'
// import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
import ConfigContext from '@/context/debug-configuration' import ConfigContext from '@/context/debug-configuration'
// import { Resolution } from '@/types/app'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
import Switch from '@/app/components/base/switch'
import type { FileUpload } from '@/app/components/base/features/types'


const ConfigVision: FC = () => { const ConfigVision: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const {
isShowVisionConfig,
visionConfig,
setVisionConfig,
} = useContext(ConfigContext)
const { isShowVisionConfig } = useContext(ConfigContext)
const file = useFeatures(s => s.features.file)
const featuresStore = useFeaturesStore()

const handleChange = useCallback((data: FileUpload) => {
const {
features,
setFeatures,
} = featuresStore!.getState()

const newFeatures = produce(features, (draft) => {
draft.file = {
...draft.file,
enabled: data.enabled,
image: {
enabled: data.enabled,
detail: data.image?.detail,
transfer_methods: data.image?.transfer_methods,
number_limits: data.image?.number_limits,
},
}
})
setFeatures(newFeatures)
}, [featuresStore])


if (!isShowVisionConfig) if (!isShowVisionConfig)
return null return null


return (<>
<Panel
className="mt-4"
headerIcon={
<Eye className='w-4 h-4 text-[#6938EF]'/>
}
title={
<div className='flex items-center'>
<div className='mr-1'>{t('appDebug.vision.name')}</div>
return (
<div className='mt-2 flex items-center gap-2 p-2 rounded-xl border-t-[0.5px] border-l-[0.5px] bg-background-section-burn'>
<div className='shrink-0 p-1'>
<div className='p-1 rounded-lg border-[0.5px] border-divider-subtle shadow-xs bg-util-colors-indigo-indigo-600'>
<Vision className='w-4 h-4 text-text-primary-on-surface' />
</div>
</div>
<div className='grow flex items-center'>
<div className='mr-1 text-text-secondary system-sm-semibold'>{t('appDebug.vision.name')}</div>
<Tooltip
popupContent={
<div className='w-[180px]' >
{t('appDebug.vision.description')}
</div>
}
/>
</div>
<div className='shrink-0 flex items-center'>
{/* <div className='mr-2 flex items-center gap-0.5'>
<div className='text-text-tertiary system-xs-medium-uppercase'>{t('appDebug.vision.visionSettings.resolution')}</div>
<Tooltip <Tooltip
popupContent={ popupContent={
<div className='w-[180px]' > <div className='w-[180px]' >
{t('appDebug.vision.description')}
{t('appDebug.vision.visionSettings.resolutionTooltip').split('\n').map(item => (
<div key={item}>{item}</div>
))}
</div> </div>
} }
/> />
</div>
}
headerRight={
<div className='flex items-center'>
<ParamConfig />
<div className='ml-4 mr-3 w-[1px] h-3.5 bg-gray-200'></div>
<Switch
defaultValue={visionConfig.enabled}
onChange={value => setVisionConfig({
...visionConfig,
enabled: value,
})}
size='md'
</div> */}
{/* <div className='flex items-center gap-1'>
<OptionCard
title={t('appDebug.vision.visionSettings.high')}
selected={file?.image?.detail === Resolution.high}
onSelect={() => handleChange(Resolution.high)}
/> />
</div>
}
noBodySpacing
/>
</>
<OptionCard
title={t('appDebug.vision.visionSettings.low')}
selected={file?.image?.detail === Resolution.low}
onSelect={() => handleChange(Resolution.low)}
/>
</div> */}
<ParamConfig />
<div className='ml-1 mr-3 w-[1px] h-3.5 bg-divider-subtle'></div>
<Switch
defaultValue={file?.enabled}
onChange={value => handleChange({
...(file || {}),
enabled: value,
})}
size='md'
/>
</div>
</div>
) )
} }
export default React.memo(ConfigVision) export default React.memo(ConfigVision)

+ 113
- 104
web/app/components/app/configuration/config-vision/param-config-content.tsx 파일 보기

'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React from 'react'
import { useContext } from 'use-context-selector'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import RadioGroup from './radio-group'
import ConfigContext from '@/context/debug-configuration'
import produce from 'immer'
import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
import { Resolution, TransferMethod } from '@/types/app' import { Resolution, TransferMethod } from '@/types/app'
import ParamItem from '@/app/components/base/param-item' import ParamItem from '@/app/components/base/param-item'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
import type { FileUpload } from '@/app/components/base/features/types'


const MIN = 1 const MIN = 1
const MAX = 6 const MAX = 6
const ParamConfigContent: FC = () => { const ParamConfigContent: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const file = useFeatures(s => s.features.file)
const featuresStore = useFeaturesStore()


const {
visionConfig,
setVisionConfig,
} = useContext(ConfigContext)
const handleChange = useCallback((data: FileUpload) => {
const {
features,
setFeatures,
} = featuresStore!.getState()


const transferMethod = (() => {
if (!visionConfig.transfer_methods || visionConfig.transfer_methods.length === 2)
return TransferMethod.all

return visionConfig.transfer_methods[0]
})()
const newFeatures = produce(features, (draft) => {
draft.file = {
...draft.file,
allowed_file_upload_methods: data.allowed_file_upload_methods,
number_limits: data.number_limits,
image: {
enabled: data.enabled,
detail: data.image?.detail,
transfer_methods: data.allowed_file_upload_methods,
number_limits: data.number_limits,
},
}
})
setFeatures(newFeatures)
}, [featuresStore])


return ( return (
<div> <div>
<div>
<div className='leading-6 text-base font-semibold text-gray-800'>{t('appDebug.vision.visionSettings.title')}</div>
<div className='pt-3 space-y-6'>
<div>
<div className='mb-2 flex items-center space-x-1'>
<div className='leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.vision.visionSettings.resolution')}</div>
<Tooltip
popupContent={
<div className='w-[180px]' >
{t('appDebug.vision.visionSettings.resolutionTooltip').split('\n').map(item => (
<div key={item}>{item}</div>
))}
</div>
}
/>
</div>
<RadioGroup
className='space-x-3'
options={[
{
label: t('appDebug.vision.visionSettings.high'),
value: Resolution.high,
},
{
label: t('appDebug.vision.visionSettings.low'),
value: Resolution.low,
},
]}
value={visionConfig.detail}
onChange={(value: Resolution) => {
setVisionConfig({
...visionConfig,
detail: value,
})
}}
<div className='leading-6 text-base font-semibold text-gray-800'>{t('appDebug.vision.visionSettings.title')}</div>
<div className='pt-3 space-y-6'>
<div>
<div className='mb-2 flex items-center space-x-1'>
<div className='leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.vision.visionSettings.resolution')}</div>
<Tooltip
popupContent={
<div className='w-[180px]' >
{t('appDebug.vision.visionSettings.resolutionTooltip').split('\n').map(item => (
<div key={item}>{item}</div>
))}
</div>
}
/> />
</div> </div>
<div>
<div className='mb-2 leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.vision.visionSettings.uploadMethod')}</div>
<RadioGroup
className='space-x-3'
options={[
{
label: t('appDebug.vision.visionSettings.both'),
value: TransferMethod.all,
},
{
label: t('appDebug.vision.visionSettings.localUpload'),
value: TransferMethod.local_file,
},
{
label: t('appDebug.vision.visionSettings.url'),
value: TransferMethod.remote_url,
},
]}
value={transferMethod}
onChange={(value: TransferMethod) => {
if (value === TransferMethod.all) {
setVisionConfig({
...visionConfig,
transfer_methods: [TransferMethod.remote_url, TransferMethod.local_file],
})
return
}
setVisionConfig({
...visionConfig,
transfer_methods: [value],
})
}}
<div className='flex items-center gap-1'>
<OptionCard
className='grow'
title={t('appDebug.vision.visionSettings.high')}
selected={file?.image?.detail === Resolution.high}
onSelect={() => handleChange({
...file,
image: { detail: Resolution.high },
})}
/>
<OptionCard
className='grow'
title={t('appDebug.vision.visionSettings.low')}
selected={file?.image?.detail === Resolution.low}
onSelect={() => handleChange({
...file,
image: { detail: Resolution.low },
})}
/> />
</div> </div>
<div>
<ParamItem
id='upload_limit'
className=''
name={t('appDebug.vision.visionSettings.uploadLimit')}
noTooltip
{...{
default: 2,
step: 1,
min: MIN,
max: MAX,
}}
value={visionConfig.number_limits}
enable={true}
onChange={(_key: string, value: number) => {
if (!value)
return

setVisionConfig({
...visionConfig,
number_limits: value,
})
}}
</div>
<div>
<div className='mb-2 leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.vision.visionSettings.uploadMethod')}</div>
<div className='flex items-center gap-1'>
<OptionCard
className='grow'
title={t('appDebug.vision.visionSettings.both')}
selected={!!file?.allowed_file_upload_methods?.includes(TransferMethod.local_file) && !!file?.allowed_file_upload_methods?.includes(TransferMethod.remote_url)}
onSelect={() => handleChange({
...file,
allowed_file_upload_methods: [TransferMethod.local_file, TransferMethod.remote_url],
})}
/>
<OptionCard
className='grow'
title={t('appDebug.vision.visionSettings.localUpload')}
selected={!!file?.allowed_file_upload_methods?.includes(TransferMethod.local_file) && file?.allowed_file_upload_methods?.length === 1}
onSelect={() => handleChange({
...file,
allowed_file_upload_methods: [TransferMethod.local_file],
})}
/>
<OptionCard
className='grow'
title={t('appDebug.vision.visionSettings.url')}
selected={!!file?.allowed_file_upload_methods?.includes(TransferMethod.remote_url) && file?.allowed_file_upload_methods?.length === 1}
onSelect={() => handleChange({
...file,
allowed_file_upload_methods: [TransferMethod.remote_url],
})}
/> />
</div> </div>
</div> </div>
<div>
<ParamItem
id='upload_limit'
className=''
name={t('appDebug.vision.visionSettings.uploadLimit')}
noTooltip
{...{
default: 2,
step: 1,
min: MIN,
max: MAX,
}}
value={file?.number_limits || 3}
enable={true}
onChange={(_key: string, value: number) => {
if (!value)
return

handleChange({
...file,
number_limits: value,
})
}}
/>
</div>
</div> </div>
</div> </div>
) )

+ 3
- 3
web/app/components/app/configuration/config-vision/param-config.tsx 파일 보기

import type { FC } from 'react' import type { FC } from 'react'
import { memo, useState } from 'react' import { memo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import VoiceParamConfig from './param-config-content'
import ParamConfigContent from './param-config-content'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general' import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
import { import {
}} }}
> >
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}> <PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
<div className={cn('flex items-center rounded-md h-7 px-3 space-x-1 text-gray-700 cursor-pointer hover:bg-gray-200', open && 'bg-gray-200')}>
<div className={cn('flex items-center rounded-md h-7 px-3 space-x-1 text-text-tertiary cursor-pointer hover:bg-gray-200', open && 'bg-gray-200')}>
<Settings01 className='w-3.5 h-3.5 ' /> <Settings01 className='w-3.5 h-3.5 ' />
<div className='ml-1 leading-[18px] text-xs font-medium '>{t('appDebug.voice.settings')}</div> <div className='ml-1 leading-[18px] text-xs font-medium '>{t('appDebug.voice.settings')}</div>
</div> </div>
</PortalToFollowElemTrigger> </PortalToFollowElemTrigger>
<PortalToFollowElemContent style={{ zIndex: 50 }}> <PortalToFollowElemContent style={{ zIndex: 50 }}>
<div className='w-80 sm:w-[412px] p-4 bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg space-y-3'> <div className='w-80 sm:w-[412px] p-4 bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg space-y-3'>
<VoiceParamConfig />
<ParamConfigContent />
</div> </div>
</PortalToFollowElemContent> </PortalToFollowElemContent>
</PortalToFollowElem> </PortalToFollowElem>

+ 0
- 40
web/app/components/app/configuration/config-vision/radio-group/index.tsx 파일 보기

'use client'
import type { FC } from 'react'
import React from 'react'
import s from './style.module.css'
import cn from '@/utils/classnames'

type OPTION = {
label: string
value: any
}

type Props = {
className?: string
options: OPTION[]
value: any
onChange: (value: any) => void
}

const RadioGroup: FC<Props> = ({
className = '',
options,
value,
onChange,
}) => {
return (
<div className={cn(className, 'flex')}>
{options.map(item => (
<div
key={item.value}
className={cn(s.item, item.value === value && s.checked)}
onClick={() => onChange(item.value)}
>
<div className={s.radio}></div>
<div className='text-[13px] font-medium text-gray-900'>{item.label}</div>
</div>
))}
</div>
)
}
export default React.memo(RadioGroup)

+ 0
- 24
web/app/components/app/configuration/config-vision/radio-group/style.module.css 파일 보기

.item {
@apply grow flex items-center h-8 px-2.5 rounded-lg bg-gray-25 border border-gray-100 cursor-pointer space-x-2;
}

.item:hover {
background-color: #ffffff;
border-color: #B2CCFF;
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
}

.item.checked {
background-color: #ffffff;
border-color: #528BFF;
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.06), 0px 1px 3px 0px rgba(16, 24, 40, 0.10);
}

.radio {
@apply w-4 h-4 border-[2px] border-gray-200 rounded-full;
}

.item.checked .radio {
border-width: 5px;
border-color: #155eef;
}

+ 0
- 220
web/app/components/app/configuration/config-voice/param-config-content.tsx 파일 보기

'use client'
import useSWR from 'swr'
import type { FC } from 'react'
import { useContext } from 'use-context-selector'
import React, { Fragment } from 'react'
import { usePathname } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import { Listbox, Transition } from '@headlessui/react'
import { CheckIcon, ChevronDownIcon } from '@heroicons/react/20/solid'
import classNames from '@/utils/classnames'
import RadioGroup from '@/app/components/app/configuration/config-vision/radio-group'
import type { Item } from '@/app/components/base/select'
import ConfigContext from '@/context/debug-configuration'
import { fetchAppVoices } from '@/service/apps'
import Tooltip from '@/app/components/base/tooltip'
import { languages } from '@/i18n/language'
import { TtsAutoPlay } from '@/types/app'
const VoiceParamConfig: FC = () => {
const { t } = useTranslation()
const pathname = usePathname()
const matched = pathname.match(/\/app\/([^/]+)/)
const appId = (matched?.length && matched[1]) ? matched[1] : ''

const {
textToSpeechConfig,
setTextToSpeechConfig,
} = useContext(ConfigContext)

let languageItem = languages.find(item => item.value === textToSpeechConfig.language)
const localLanguagePlaceholder = languageItem?.name || t('common.placeholder.select')
if (languages && !languageItem && languages.length > 0)
languageItem = languages[0]
const language = languageItem?.value
const voiceItems = useSWR({ appId, language }, fetchAppVoices).data
let voiceItem = voiceItems?.find(item => item.value === textToSpeechConfig.voice)
if (voiceItems && !voiceItem && voiceItems.length > 0)
voiceItem = voiceItems[0]

const localVoicePlaceholder = voiceItem?.name || t('common.placeholder.select')

return (
<div>
<div>
<div className='leading-6 text-base font-semibold text-gray-800'>{t('appDebug.voice.voiceSettings.title')}</div>
<div className='pt-3 space-y-6'>
<div>
<div className='mb-2 flex items-center space-x-1'>
<div
className='leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.voice.voiceSettings.language')}</div>
<Tooltip
popupContent={
<div className='w-[180px]'>
{t('appDebug.voice.voiceSettings.resolutionTooltip').split('\n').map(item => (
<div key={item}>{item}</div>
))}
</div>
}
/>
</div>
<Listbox
value={languageItem}
onChange={(value: Item) => {
setTextToSpeechConfig({
...textToSpeechConfig,
language: String(value.value),
})
}}
>
<div className={'relative h-9'}>
<Listbox.Button
className={'w-full h-full rounded-lg border-0 bg-gray-100 py-1.5 pl-3 pr-10 sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200 cursor-pointer'}>
<span className={classNames('block truncate text-left', !languageItem?.name && 'text-gray-400')}>
{languageItem?.name ? t(`common.voice.language.${languageItem?.value.replace('-', '')}`) : localLanguagePlaceholder}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronDownIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</span>
</Listbox.Button>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>

<Listbox.Options
className="absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg border-gray-200 border-[0.5px] focus:outline-none sm:text-sm">
{languages.map((item: Item) => (
<Listbox.Option
key={item.value}
className={({ active }) =>
`relative cursor-pointer select-none py-2 pl-3 pr-9 rounded-lg hover:bg-gray-100 text-gray-700 ${active ? 'bg-gray-100' : ''
}`
}
value={item}
disabled={false}
>
{({ /* active, */ selected }) => (
<>
<span
className={classNames('block', selected && 'font-normal')}>{t(`common.voice.language.${(item.value).toString().replace('-', '')}`)}</span>
{(selected || item.value === textToSpeechConfig.language) && (
<span
className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-4 text-gray-700',
)}
>
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
)}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</Listbox>
</div>
<div>
<div
className='mb-2 leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.voice.voiceSettings.voice')}</div>
<Listbox
value={voiceItem ?? {}}
disabled={!languageItem}
onChange={(value: Item) => {
if (!value.value)
return
setTextToSpeechConfig({
...textToSpeechConfig,
voice: String(value.value),
})
}}
>
<div className={'relative h-9'}>
<Listbox.Button
className={'w-full h-full rounded-lg border-0 bg-gray-100 py-1.5 pl-3 pr-10 sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200 cursor-pointer'}>
<span
className={classNames('block truncate text-left', !voiceItem?.name && 'text-gray-400')}>{voiceItem?.name ?? localVoicePlaceholder}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronDownIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</span>
</Listbox.Button>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>

<Listbox.Options
className="absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg border-gray-200 border-[0.5px] focus:outline-none sm:text-sm">
{voiceItems?.map((item: Item) => (
<Listbox.Option
key={item.value}
className={({ active }) =>
`relative cursor-pointer select-none py-2 pl-3 pr-9 rounded-lg hover:bg-gray-100 text-gray-700 ${active ? 'bg-gray-100' : ''
}`
}
value={item}
disabled={false}
>
{({ /* active, */ selected }) => (
<>
<span className={classNames('block', selected && 'font-normal')}>{item.name}</span>
{(selected || item.value === textToSpeechConfig.voice) && (
<span
className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-4 text-gray-700',
)}
>
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
)}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</Listbox>
</div>
<div>
<div
className='mb-2 leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.voice.voiceSettings.autoPlay')}</div>
<RadioGroup
className='space-x-3'
options={[
{
label: t('appDebug.voice.voiceSettings.autoPlayEnabled'),
value: TtsAutoPlay.enabled,
},
{
label: t('appDebug.voice.voiceSettings.autoPlayDisabled'),
value: TtsAutoPlay.disabled,
},
]}
value={textToSpeechConfig.autoPlay ? textToSpeechConfig.autoPlay : TtsAutoPlay.disabled}
onChange={(value: TtsAutoPlay) => {
setTextToSpeechConfig({
...textToSpeechConfig,
autoPlay: value,
})
}}
/>
</div>
</div>
</div>
</div>
)
}

export default React.memo(VoiceParamConfig)

+ 0
- 41
web/app/components/app/configuration/config-voice/param-config.tsx 파일 보기

'use client'
import type { FC } from 'react'
import { memo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import VoiceParamConfig from './param-config-content'
import cn from '@/utils/classnames'
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'

const ParamsConfig: FC = () => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)

return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
offset={{
mainAxis: 4,
}}
>
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
<div className={cn('flex items-center rounded-md h-7 px-3 space-x-1 text-gray-700 cursor-pointer hover:bg-gray-200', open && 'bg-gray-200')}>
<Settings01 className='w-3.5 h-3.5 ' />
<div className='ml-1 leading-[18px] text-xs font-medium '>{t('appDebug.voice.settings')}</div>
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent style={{ zIndex: 50 }}>
<div className='w-80 sm:w-[412px] p-4 bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg space-y-3'>
<VoiceParamConfig />
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default memo(ParamsConfig)

+ 1
- 1
web/app/components/app/configuration/config/agent/agent-tools/index.tsx 파일 보기

return ( return (
<> <>
<Panel <Panel
className="mt-4"
className="mt-2"
noBodySpacing={tools.length === 0} noBodySpacing={tools.length === 0}
headerIcon={ headerIcon={
<RiHammerFill className='w-4 h-4 text-primary-500' /> <RiHammerFill className='w-4 h-4 text-primary-500' />

+ 20
- 6
web/app/components/app/configuration/config/automatic/get-automatic-res.tsx 파일 보기

import s from './style.module.css' import s from './style.module.css'
import Modal from '@/app/components/base/modal' import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Textarea from '@/app/components/base/textarea'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import { generateRule } from '@/service/debug' import { generateRule } from '@/service/debug'
import ConfigPrompt from '@/app/components/app/configuration/config-prompt' import ConfigPrompt from '@/app/components/app/configuration/config-prompt'
import type { Model } from '@/types/app' import type { Model } from '@/types/app'
import { AppType } from '@/types/app' import { AppType } from '@/types/app'
import ConfigVar from '@/app/components/app/configuration/config-var' import ConfigVar from '@/app/components/app/configuration/config-var'
import OpeningStatement from '@/app/components/app/configuration/features/chat-group/opening-statement'
import GroupName from '@/app/components/app/configuration/base/group-name' import GroupName from '@/app/components/app/configuration/base/group-name'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import Confirm from '@/app/components/base/confirm' import Confirm from '@/app/components/base/confirm'
import { LoveMessage } from '@/app/components/base/icons/src/vender/features'


// type // type
import type { AutomaticRes } from '@/service/debug' import type { AutomaticRes } from '@/service/debug'
<div className='mt-6'> <div className='mt-6'>
<div className='text-[0px]'> <div className='text-[0px]'>
<div className='mb-2 leading-5 text-sm font-medium text-gray-900'>{t('appDebug.generate.instruction')}</div> <div className='mb-2 leading-5 text-sm font-medium text-gray-900'>{t('appDebug.generate.instruction')}</div>
<textarea className="w-full h-[200px] overflow-y-auto px-3 py-2 text-sm bg-gray-50 rounded-lg" placeholder={t('appDebug.generate.instructionPlaceHolder') as string} value={instruction} onChange={e => setInstruction(e.target.value)} />
<Textarea
className="h-[200px] resize-none"
placeholder={t('appDebug.generate.instructionPlaceHolder') as string}
value={instruction}
onChange={e => setInstruction(e.target.value)} />
</div> </div>


<div className='mt-5 flex justify-end'> <div className='mt-5 flex justify-end'>
{(mode !== AppType.completion && res?.opening_statement) && ( {(mode !== AppType.completion && res?.opening_statement) && (
<div className='mt-7'> <div className='mt-7'>
<GroupName name={t('appDebug.feature.groupChat.title')} /> <GroupName name={t('appDebug.feature.groupChat.title')} />
<OpeningStatement
value={res?.opening_statement || ''}
readonly
/>
<div
className='mb-1 p-3 border-t-[0.5px] border-l-[0.5px] border-effects-highlight rounded-xl bg-background-section-burn'
>
<div className='mb-2 flex items-center gap-2'>
<div className='shrink-0 p-1 rounded-lg border-[0.5px] border-divider-subtle shadow-xs bg-util-colors-blue-light-blue-light-500'>
<LoveMessage className='w-4 h-4 text-text-primary-on-surface' />
</div>
<div className='grow flex items-center text-text-secondary system-sm-semibold'>
{t('appDebug.feature.conversationOpener.title')}
</div>
</div>
<div className='min-h-8 text-text-tertiary system-xs-regular'>{res.opening_statement}</div>
</div>
</div> </div>
)} )}
</> </>

+ 0
- 40
web/app/components/app/configuration/config/feature/add-feature-btn/index.tsx 파일 보기

'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { PlusIcon } from '@heroicons/react/24/solid'

export type IAddFeatureBtnProps = {
toBottomHeight: number
onClick: () => void
}

const ITEM_HEIGHT = 48

const AddFeatureBtn: FC<IAddFeatureBtnProps> = ({
toBottomHeight,
onClick,
}) => {
const { t } = useTranslation()
return (
<div
className='absolute z-[9] left-0 right-0 flex justify-center pb-4'
style={{
top: toBottomHeight - ITEM_HEIGHT,
background: 'linear-gradient(180deg, rgba(255, 255, 255, 0.00) 0%, #FFF 100%)',
}}
>
<div
className='flex items-center h-8 space-x-2 px-3
border border-primary-100 rounded-lg bg-primary-25 hover:bg-primary-50 cursor-pointer
text-xs font-semibold text-primary-600 uppercase
'
onClick={onClick}
>
<PlusIcon className='w-4 h-4 font-semibold' />
<div>{t('appDebug.operation.addFeature')}</div>
</div>
</div>
)
}
export default React.memo(AddFeatureBtn)

+ 0
- 52
web/app/components/app/configuration/config/feature/choose-feature/feature-item/index.tsx 파일 보기

'use client'
import type { FC } from 'react'
import React from 'react'
import s from './style.module.css'
import cn from '@/utils/classnames'
import Switch from '@/app/components/base/switch'

export type IFeatureItemProps = {
icon: React.ReactNode
previewImgClassName?: string
title: string
description: string
value: boolean
onChange: (value: boolean) => void
}

const FeatureItem: FC<IFeatureItemProps> = ({
icon,
previewImgClassName,
title,
description,
value,
onChange,
}) => {
return (
<div className={cn(s.wrap, 'relative flex justify-between p-3 rounded-xl border border-transparent bg-gray-50 hover:border-gray-200 cursor-pointer')}>
<div className='flex space-x-3 mr-2'>
{/* icon */}
<div
className='shrink-0 flex items-center justify-center w-8 h-8 rounded-lg border border-gray-200 bg-white'
style={{
boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)',
}}
>
{icon}
</div>
<div>
<div className='text-sm font-semibold text-gray-800'>{title}</div>
<div className='text-xs font-normal text-gray-500'>{description}</div>
</div>
</div>

<Switch onChange={onChange} defaultValue={value} />
{
previewImgClassName && (
<div className={cn(s.preview, s[previewImgClassName])}>
</div>)
}
</div>
)
}
export default React.memo(FeatureItem)

BIN
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/citation.png 파일 보기


+ 0
- 150
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/citation.svg
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
파일 보기


BIN
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/citations-and-attributions-preview@2x.png 파일 보기


BIN
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/conversation-opener-preview@2x.png 파일 보기


BIN
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/more-like-this-preview@2x.png 파일 보기


BIN
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/more-like-this.png 파일 보기


+ 0
- 188
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/more-like-this.svg
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
파일 보기


BIN
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/next-question-suggestion-preview@2x.png 파일 보기


BIN
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/opening-statement.png 파일 보기


BIN
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/opening-suggestion-preview@2x.png 파일 보기


BIN
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/speech-to-text-preview@2x.png 파일 보기


BIN
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/speech-to-text.png 파일 보기


+ 0
- 100
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/speech-to-text.svg
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
파일 보기


BIN
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/suggested-questions-after-answer.png 파일 보기


+ 0
- 163
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/suggested-questions-after-answer.svg
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
파일 보기


BIN
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/text-to-audio-preview-assistant@2x.png 파일 보기


BIN
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/text-to-audio-preview-completion@2x.png 파일 보기


+ 0
- 41
web/app/components/app/configuration/config/feature/choose-feature/feature-item/style.module.css 파일 보기

.preview {
display: none;
position: absolute;
top: 0;
left: 100%;
transform: translate(32px, -54px);
width: 280px;
height: 360px;
background: center center no-repeat;
background-size: contain;
border-radius: 8px;
}

.wrap:hover .preview {
display: block;
}

.openingStatementPreview {
background-image: url(./preview-imgs/opening-statement.png);
}

.suggestedQuestionsAfterAnswerPreview {
background-image: url(./preview-imgs/suggested-questions-after-answer.png);
}

.moreLikeThisPreview {
background-image: url(./preview-imgs/more-like-this.png);
}

.speechToTextPreview {
background-image: url(./preview-imgs/speech-to-text.png);
}

.textToSpeechPreview {
@apply shadow-lg rounded-lg;
background-image: url(./preview-imgs/text-to-audio-preview-assistant@2x.png);
}

.citationPreview {
background-image: url(./preview-imgs/citation.png);
}

+ 0
- 172
web/app/components/app/configuration/config/feature/choose-feature/index.tsx 파일 보기

'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import FeatureGroup from '../feature-group'
import MoreLikeThisIcon from '../../../base/icons/more-like-this-icon'
import FeatureItem from './feature-item'
import Modal from '@/app/components/base/modal'
import SuggestedQuestionsAfterAnswerIcon from '@/app/components/app/configuration/base/icons/suggested-questions-after-answer-icon'
import { Microphone01, Speaker } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
import { Citations } from '@/app/components/base/icons/src/vender/solid/editor'
import { FileSearch02 } from '@/app/components/base/icons/src/vender/solid/files'
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
type IConfig = {
openingStatement: boolean
moreLikeThis: boolean
suggestedQuestionsAfterAnswer: boolean
speechToText: boolean
textToSpeech: boolean
citation: boolean
moderation: boolean
annotation: boolean
}

export type IChooseFeatureProps = {
isShow: boolean
onClose: () => void
config: IConfig
isChatApp: boolean
onChange: (key: string, value: boolean) => void
showTextToSpeechItem?: boolean
showSpeechToTextItem?: boolean
}

const OpeningStatementIcon = (
<svg width="15" height="13" viewBox="0 0 15 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M8.33328 0.333252C4.83548 0.333252 1.99995 3.16878 1.99995 6.66659C1.99995 7.37325 2.11594 8.05419 2.33045 8.6906C2.36818 8.80254 2.39039 8.86877 2.40482 8.91762L2.40955 8.93407L2.40705 8.93928C2.38991 8.97462 2.36444 9.02207 2.31681 9.11025L1.21555 11.1486C1.1473 11.2749 1.07608 11.4066 1.02711 11.5212C0.978424 11.6351 0.899569 11.844 0.938369 12.0916C0.98385 12.3819 1.15471 12.6375 1.40556 12.7905C1.61957 12.9211 1.84276 12.9281 1.96659 12.9267C2.09117 12.9252 2.24012 12.9098 2.3829 12.895L5.81954 12.5397C5.87458 12.534 5.90335 12.5311 5.92443 12.5295L5.92715 12.5293L5.93539 12.5322C5.96129 12.5415 5.99642 12.555 6.05705 12.5784C6.76435 12.8509 7.53219 12.9999 8.33328 12.9999C11.8311 12.9999 14.6666 10.1644 14.6666 6.66659C14.6666 3.16878 11.8311 0.333252 8.33328 0.333252ZM5.97966 4.7214C6.73118 4.08722 7.73139 4.27352 8.3312 4.96609C8.931 4.27352 9.9183 4.09389 10.6827 4.7214C11.4472 5.34892 11.5401 6.41591 10.9499 7.16596C10.5843 7.63065 9.66655 8.47935 9.02117 9.05789C8.78411 9.2704 8.66558 9.37666 8.52332 9.41947C8.40129 9.4562 8.2611 9.4562 8.13907 9.41947C7.99682 9.37666 7.87829 9.2704 7.64122 9.05789C6.99584 8.47935 6.07814 7.63065 5.71251 7.16596C5.12234 6.41591 5.22815 5.35559 5.97966 4.7214Z" fill="#DD2590" />
</svg>
)

const ChooseFeature: FC<IChooseFeatureProps> = ({
isShow,
onClose,
isChatApp,
config,
onChange,
showTextToSpeechItem,
showSpeechToTextItem,
}) => {
const { t } = useTranslation()
return (
<Modal
isShow={isShow}
onClose={onClose}
className='w-[400px]'
title={t('appDebug.operation.addFeature')}
closable
overflowVisible
>
<div className='pt-5 pb-10'>
{/* Chat Feature */}
{isChatApp && (
<FeatureGroup
title={t('appDebug.feature.groupChat.title')}
description={t('appDebug.feature.groupChat.description') as string}
>
<>
<FeatureItem
icon={OpeningStatementIcon}
previewImgClassName='openingStatementPreview'
title={t('appDebug.feature.conversationOpener.title')}
description={t('appDebug.feature.conversationOpener.description')}
value={config.openingStatement}
onChange={value => onChange('openingStatement', value)}
/>
<FeatureItem
icon={<SuggestedQuestionsAfterAnswerIcon />}
previewImgClassName='suggestedQuestionsAfterAnswerPreview'
title={t('appDebug.feature.suggestedQuestionsAfterAnswer.title')}
description={t('appDebug.feature.suggestedQuestionsAfterAnswer.description')}
value={config.suggestedQuestionsAfterAnswer}
onChange={value => onChange('suggestedQuestionsAfterAnswer', value)}
/>
{
showTextToSpeechItem && (
<FeatureItem
icon={<Speaker className='w-4 h-4 text-[#7839EE]' />}
previewImgClassName='textToSpeechPreview'
title={t('appDebug.feature.textToSpeech.title')}
description={t('appDebug.feature.textToSpeech.description')}
value={config.textToSpeech}
onChange={value => onChange('textToSpeech', value)}
/>
)
}
{
showSpeechToTextItem && (
<FeatureItem
icon={<Microphone01 className='w-4 h-4 text-[#7839EE]' />}
previewImgClassName='speechToTextPreview'
title={t('appDebug.feature.speechToText.title')}
description={t('appDebug.feature.speechToText.description')}
value={config.speechToText}
onChange={value => onChange('speechToText', value)}
/>
)
}
<FeatureItem
icon={<Citations className='w-4 h-4 text-[#FD853A]' />}
previewImgClassName='citationPreview'
title={t('appDebug.feature.citation.title')}
description={t('appDebug.feature.citation.description')}
value={config.citation}
onChange={value => onChange('citation', value)}
/>
</>
</FeatureGroup>
)}

{/* Text Generation Feature */}
{!isChatApp && (
<FeatureGroup title={t('appDebug.feature.groupExperience.title')}>
<>
<FeatureItem
icon={<MoreLikeThisIcon />}
previewImgClassName='moreLikeThisPreview'
title={t('appDebug.feature.moreLikeThis.title')}
description={t('appDebug.feature.moreLikeThis.description')}
value={config.moreLikeThis}
onChange={value => onChange('moreLikeThis', value)}
/>
{
showTextToSpeechItem && (
<FeatureItem
icon={<Speaker className='w-4 h-4 text-[#7839EE]' />}
previewImgClassName='textToSpeechPreview'
title={t('appDebug.feature.textToSpeech.title')}
description={t('appDebug.feature.textToSpeech.description')}
value={config.textToSpeech}
onChange={value => onChange('textToSpeech', value)}
/>
)
}
</>
</FeatureGroup>
)}
<FeatureGroup title={t('appDebug.feature.toolbox.title')}>
<>
<FeatureItem
icon={<FileSearch02 className='w-4 h-4 text-[#039855]' />}
previewImgClassName=''
title={t('appDebug.feature.moderation.title')}
description={t('appDebug.feature.moderation.description')}
value={config.moderation}
onChange={value => onChange('moderation', value)}
/>
{isChatApp && (
<FeatureItem
icon={<MessageFast className='w-4 h-4 text-[#444CE7]' />}
title={t('appDebug.feature.annotation.title')}
description={t('appDebug.feature.annotation.description')}
value={config.annotation}
onChange={value => onChange('annotation', value)}
/>
)}
</>
</FeatureGroup>
</div>
</Modal>
)
}
export default React.memo(ChooseFeature)

+ 0
- 31
web/app/components/app/configuration/config/feature/feature-group/index.tsx 파일 보기

'use client'
import type { FC } from 'react'
import React from 'react'
import GroupName from '@/app/components/app/configuration/base/group-name'

export type IFeatureGroupProps = {
title: string
description?: string
children: React.ReactNode
}

const FeatureGroup: FC<IFeatureGroupProps> = ({
title,
description,
children,
}) => {
return (
<div className='mb-6'>
<div className='mb-2'>
<GroupName name={title} />
{description && (
<div className='text-xs font-normal text-gray-500'>{description}</div>
)}
</div>
<div className='space-y-2'>
{children}
</div>
</div>
)
}
export default React.memo(FeatureGroup)

+ 2
- 229
web/app/components/app/configuration/config/index.tsx 파일 보기

'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React, { useRef } from 'react'
import React from 'react'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import produce from 'immer' import produce from 'immer'
import { useBoolean, useScroll } from 'ahooks'
import { useFormattingChangedDispatcher } from '../debug/hooks' import { useFormattingChangedDispatcher } from '../debug/hooks'
import DatasetConfig from '../dataset-config' import DatasetConfig from '../dataset-config'
import ChatGroup from '../features/chat-group'
import ExperienceEnhanceGroup from '../features/experience-enhance-group'
import Toolbox from '../toolbox'
import HistoryPanel from '../config-prompt/conversation-history/history-panel' import HistoryPanel from '../config-prompt/conversation-history/history-panel'
import ConfigVision from '../config-vision' import ConfigVision from '../config-vision'
import useAnnotationConfig from '../toolbox/annotation/use-annotation-config'
import AddFeatureBtn from './feature/add-feature-btn'
import ChooseFeature from './feature/choose-feature'
import useFeature from './feature/use-feature'
import AgentTools from './agent/agent-tools' import AgentTools from './agent/agent-tools'
import ConfigContext from '@/context/debug-configuration' import ConfigContext from '@/context/debug-configuration'
import ConfigPrompt from '@/app/components/app/configuration/config-prompt' import ConfigPrompt from '@/app/components/app/configuration/config-prompt'
import ConfigVar from '@/app/components/app/configuration/config-var' import ConfigVar from '@/app/components/app/configuration/config-var'
import { type CitationConfig, type ModelConfig, type ModerationConfig, type MoreLikeThisConfig, type PromptVariable, type SpeechToTextConfig, type SuggestedQuestionsAfterAnswerConfig, type TextToSpeechConfig } from '@/models/debug'
import { type ModelConfig, type PromptVariable } from '@/models/debug'
import type { AppType } from '@/types/app' import type { AppType } from '@/types/app'
import { ModelModeType } from '@/types/app' import { ModelModeType } from '@/types/app'
import { useModalContext } from '@/context/modal-context'
import ConfigParamModal from '@/app/components/app/configuration/toolbox/annotation/config-param-modal'
import AnnotationFullModal from '@/app/components/billing/annotation-full/modal'
import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'


const Config: FC = () => { const Config: FC = () => {
const { const {
appId,
mode, mode,
isAdvancedMode, isAdvancedMode,
modelModeType, modelModeType,
isAgent, isAgent,
// canReturnToSimpleMode,
// setPromptMode,
hasSetBlockStatus, hasSetBlockStatus,
showHistoryModal, showHistoryModal,
introduction,
setIntroduction,
suggestedQuestions,
setSuggestedQuestions,
modelConfig, modelConfig,
setModelConfig, setModelConfig,
setPrevPromptConfig, setPrevPromptConfig,
moreLikeThisConfig,
setMoreLikeThisConfig,
suggestedQuestionsAfterAnswerConfig,
setSuggestedQuestionsAfterAnswerConfig,
speechToTextConfig,
setSpeechToTextConfig,
textToSpeechConfig,
setTextToSpeechConfig,
citationConfig,
setCitationConfig,
annotationConfig,
setAnnotationConfig,
moderationConfig,
setModerationConfig,
} = useContext(ConfigContext) } = useContext(ConfigContext)
const isChatApp = ['advanced-chat', 'agent-chat', 'chat'].includes(mode) const isChatApp = ['advanced-chat', 'agent-chat', 'chat'].includes(mode)
const { data: speech2textDefaultModel } = useDefaultModel(ModelTypeEnum.speech2text)
const { data: text2speechDefaultModel } = useDefaultModel(ModelTypeEnum.tts)
const { setShowModerationSettingModal } = useModalContext()
const formattingChangedDispatcher = useFormattingChangedDispatcher() const formattingChangedDispatcher = useFormattingChangedDispatcher()


const promptTemplate = modelConfig.configs.prompt_template const promptTemplate = modelConfig.configs.prompt_template
setModelConfig(newModelConfig) setModelConfig(newModelConfig)
} }


const [showChooseFeature, {
setTrue: showChooseFeatureTrue,
setFalse: showChooseFeatureFalse,
}] = useBoolean(false)
const { featureConfig, handleFeatureChange } = useFeature({
introduction,
setIntroduction,
moreLikeThis: moreLikeThisConfig.enabled,
setMoreLikeThis: (value) => {
setMoreLikeThisConfig(produce(moreLikeThisConfig, (draft: MoreLikeThisConfig) => {
draft.enabled = value
}))
},
suggestedQuestionsAfterAnswer: suggestedQuestionsAfterAnswerConfig.enabled,
setSuggestedQuestionsAfterAnswer: (value) => {
setSuggestedQuestionsAfterAnswerConfig(produce(suggestedQuestionsAfterAnswerConfig, (draft: SuggestedQuestionsAfterAnswerConfig) => {
draft.enabled = value
}))
formattingChangedDispatcher()
},
speechToText: speechToTextConfig.enabled,
setSpeechToText: (value) => {
setSpeechToTextConfig(produce(speechToTextConfig, (draft: SpeechToTextConfig) => {
draft.enabled = value
}))
},
textToSpeech: textToSpeechConfig.enabled,
setTextToSpeech: (value) => {
setTextToSpeechConfig(produce(textToSpeechConfig, (draft: TextToSpeechConfig) => {
draft.enabled = value
draft.voice = textToSpeechConfig?.voice
draft.language = textToSpeechConfig?.language
}))
},
citation: citationConfig.enabled,
setCitation: (value) => {
setCitationConfig(produce(citationConfig, (draft: CitationConfig) => {
draft.enabled = value
}))
formattingChangedDispatcher()
},
annotation: annotationConfig.enabled,
setAnnotation: async (value) => {
if (value) {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
setIsShowAnnotationConfigInit(true)
}
else {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
await handleDisableAnnotation(annotationConfig.embedding_model)
}
},
moderation: moderationConfig.enabled,
setModeration: (value) => {
setModerationConfig(produce(moderationConfig, (draft: ModerationConfig) => {
draft.enabled = value
}))
if (value && !moderationConfig.type) {
setShowModerationSettingModal({
payload: {
enabled: true,
type: 'keywords',
config: {
keywords: '',
inputs_config: {
enabled: true,
preset_response: '',
},
},
},
onSaveCallback: setModerationConfig,
onCancelCallback: () => {
setModerationConfig(produce(moderationConfig, (draft: ModerationConfig) => {
draft.enabled = false
showChooseFeatureTrue()
}))
},
})
showChooseFeatureFalse()
}
},
})

const {
handleEnableAnnotation,
setScore,
handleDisableAnnotation,
isShowAnnotationConfigInit,
setIsShowAnnotationConfigInit,
isShowAnnotationFullModal,
setIsShowAnnotationFullModal,
} = useAnnotationConfig({
appId,
annotationConfig,
setAnnotationConfig,
})

const hasChatConfig = isChatApp && (featureConfig.openingStatement || featureConfig.suggestedQuestionsAfterAnswer || (featureConfig.speechToText && !!speech2textDefaultModel) || (featureConfig.textToSpeech && !!text2speechDefaultModel) || featureConfig.citation)
const hasCompletionConfig = !isChatApp && (moreLikeThisConfig.enabled || (featureConfig.textToSpeech && !!text2speechDefaultModel))

const hasToolbox = moderationConfig.enabled || featureConfig.annotation

const wrapRef = useRef<HTMLDivElement>(null)
const wrapScroll = useScroll(wrapRef)
const toBottomHeight = (() => {
if (!wrapRef.current)
return 999
const elem = wrapRef.current
const { clientHeight } = elem
const value = (wrapScroll?.top || 0) + clientHeight
return value
})()

return ( return (
<> <>
<div <div
ref={wrapRef}
className="grow h-0 relative px-6 pb-[50px] overflow-y-auto" className="grow h-0 relative px-6 pb-[50px] overflow-y-auto"
> >
<AddFeatureBtn toBottomHeight={toBottomHeight} onClick={showChooseFeatureTrue} />
{showChooseFeature && (
<ChooseFeature
isShow={showChooseFeature}
onClose={showChooseFeatureFalse}
isChatApp={isChatApp}
config={featureConfig}
onChange={handleFeatureChange}
showSpeechToTextItem={!!speech2textDefaultModel}
showTextToSpeechItem={!!text2speechDefaultModel}
/>
)}

{/* Template */} {/* Template */}
<ConfigPrompt <ConfigPrompt
mode={mode as AppType} mode={mode as AppType}
onShowEditModal={showHistoryModal} onShowEditModal={showHistoryModal}
/> />
)} )}

{/* ChatConfig */}
{
hasChatConfig && (
<ChatGroup
isShowOpeningStatement={featureConfig.openingStatement}
openingStatementConfig={
{
value: introduction,
onChange: setIntroduction,
suggestedQuestions,
onSuggestedQuestionsChange: setSuggestedQuestions,
}
}
isShowSuggestedQuestionsAfterAnswer={featureConfig.suggestedQuestionsAfterAnswer}
isShowTextToSpeech={featureConfig.textToSpeech && !!text2speechDefaultModel}
isShowSpeechText={featureConfig.speechToText && !!speech2textDefaultModel}
isShowCitation={featureConfig.citation}
/>
)
}

{/* Text Generation config */}{
hasCompletionConfig && (
<ExperienceEnhanceGroup
isShowMoreLike={moreLikeThisConfig.enabled}
isShowTextToSpeech={featureConfig.textToSpeech && !!text2speechDefaultModel}
/>
)
}

{/* Toolbox */}
{
hasToolbox && (
<Toolbox
showModerationSettings={moderationConfig.enabled}
showAnnotation={isChatApp && featureConfig.annotation}
onEmbeddingChange={handleEnableAnnotation}
onScoreChange={setScore}
/>
)
}

<ConfigParamModal
appId={appId}
isInit
isShow={isShowAnnotationConfigInit}
onHide={() => {
setIsShowAnnotationConfigInit(false)
showChooseFeatureTrue()
}}
onSave={async (embeddingModel, score) => {
await handleEnableAnnotation(embeddingModel, score)
setIsShowAnnotationConfigInit(false)
}}
annotationConfig={annotationConfig}
/>
{isShowAnnotationFullModal && (
<AnnotationFullModal
show={isShowAnnotationFullModal}
onHide={() => setIsShowAnnotationFullModal(false)}
/>
)}
</div> </div>
</> </>
) )

+ 1
- 1
web/app/components/app/configuration/dataset-config/index.tsx 파일 보기



return ( return (
<FeaturePanel <FeaturePanel
className='mt-3'
className='mt-2'
headerIcon={Icon} headerIcon={Icon}
title={t('appDebug.feature.dataSet.title')} title={t('appDebug.feature.dataSet.title')}
headerRight={ headerRight={

+ 6
- 4
web/app/components/app/configuration/dataset-config/settings-modal/index.tsx 파일 보기

import IndexMethodRadio from '@/app/components/datasets/settings/index-method-radio' import IndexMethodRadio from '@/app/components/datasets/settings/index-method-radio'
import Divider from '@/app/components/base/divider' import Divider from '@/app/components/base/divider'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import type { DataSet } from '@/models/datasets' import type { DataSet } from '@/models/datasets'
import { useToastContext } from '@/app/components/base/toast' import { useToastContext } from '@/app/components/base/toast'
import { updateDatasetSetting } from '@/service/datasets' import { updateDatasetSetting } from '@/service/datasets'
<div className={labelClass}> <div className={labelClass}>
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.name')}</div> <div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.name')}</div>
</div> </div>
<input
<Input
value={localeCurrentDataset.name} value={localeCurrentDataset.name}
onChange={e => handleValueChange('name', e.target.value)} onChange={e => handleValueChange('name', e.target.value)}
className='block px-3 w-full h-9 bg-gray-100 rounded-lg text-sm text-gray-900 outline-none appearance-none'
className='block h-9'
placeholder={t('datasetSettings.form.namePlaceholder') || ''} placeholder={t('datasetSettings.form.namePlaceholder') || ''}
/> />
</div> </div>
<div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.desc')}</div> <div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.desc')}</div>
</div> </div>
<div className='w-full'> <div className='w-full'>
<textarea
<Textarea
value={localeCurrentDataset.description || ''} value={localeCurrentDataset.description || ''}
onChange={e => handleValueChange('description', e.target.value)} onChange={e => handleValueChange('description', e.target.value)}
className='block px-3 py-2 w-full h-[88px] rounded-lg bg-gray-100 text-sm outline-none appearance-none resize-none'
className='resize-none'
placeholder={t('datasetSettings.form.descPlaceholder') || ''} placeholder={t('datasetSettings.form.descPlaceholder') || ''}
/> />
<a className='mt-2 flex items-center h-[18px] px-3 text-xs text-gray-500' href="https://docs.dify.ai/features/datasets#how-to-write-a-good-dataset-description" target='_blank' rel='noopener noreferrer'> <a className='mt-2 flex items-center h-[18px] px-3 text-xs text-gray-500' href="https://docs.dify.ai/features/datasets#how-to-write-a-good-dataset-description" target='_blank' rel='noopener noreferrer'>

+ 109
- 0
web/app/components/app/configuration/debug/chat-user-input.tsx 파일 보기

import React from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import ConfigContext from '@/context/debug-configuration'
import Input from '@/app/components/base/input'
import Select from '@/app/components/base/select'
import Textarea from '@/app/components/base/textarea'
import { DEFAULT_VALUE_MAX_LEN } from '@/config'
import type { Inputs } from '@/models/debug'
import cn from '@/utils/classnames'

type Props = {
inputs: Inputs
}

const ChatUserInput = ({
inputs,
}: Props) => {
const { t } = useTranslation()
const { modelConfig, setInputs } = useContext(ConfigContext)

const promptVariables = modelConfig.configs.prompt_variables.filter(({ key, name }) => {
return key && key?.trim() && name && name?.trim()
})

const promptVariableObj = (() => {
const obj: Record<string, boolean> = {}
promptVariables.forEach((input) => {
obj[input.key] = true
})
return obj
})()

const handleInputValueChange = (key: string, value: string) => {
if (!(key in promptVariableObj))
return

const newInputs = { ...inputs }
promptVariables.forEach((input) => {
if (input.key === key)
newInputs[key] = value
})
setInputs(newInputs)
}

if (!promptVariables.length)
return null

return (
<div className={cn('bg-components-panel-on-panel-item-bg rounded-xl border-[0.5px] border-components-panel-border-subtle shadow-xs z-[1]')}>
<div className='px-4 pt-3 pb-4'>
{promptVariables.map(({ key, name, type, options, max_length, required }, index) => (
<div
key={key}
className='mb-4 last-of-type:mb-0'
>
<div>
<div className='h-6 mb-1 flex items-center gap-1 text-text-secondary system-sm-semibold'>
<div className='truncate'>{name || key}</div>
{!required && <span className='text-text-tertiary system-xs-regular'>{t('workflow.panel.optional')}</span>}
</div>
<div className='grow'>
{type === 'string' && (
<Input
value={inputs[key] ? `${inputs[key]}` : ''}
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
placeholder={name}
autoFocus={index === 0}
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
/>
)}
{type === 'paragraph' && (
<Textarea
className='grow h-[120px]'
placeholder={name}
value={inputs[key] ? `${inputs[key]}` : ''}
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
/>
)}
{type === 'select' && (
<Select
className='w-full'
defaultValue={inputs[key] as string}
onSelect={(i) => { handleInputValueChange(key, i.value as string) }}
items={(options || []).map(i => ({ name: i, value: i }))}
allowSearch={false}
bgClassName='bg-gray-50'
/>
)}
{type === 'number' && (
<Input
type='number'
value={inputs[key] ? `${inputs[key]}` : ''}
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
placeholder={name}
autoFocus={index === 0}
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
/>
)}
</div>
</div>
</div>
))}
</div>
</div>
)
}

export default ChatUserInput

+ 26
- 6
web/app/components/app/configuration/debug/debug-with-multiple-model/chat-item.tsx 파일 보기

import Chat from '@/app/components/base/chat/chat' import Chat from '@/app/components/base/chat/chat'
import { useChat } from '@/app/components/base/chat/chat/hooks' import { useChat } from '@/app/components/base/chat/chat/hooks'
import { useDebugConfigurationContext } from '@/context/debug-configuration' import { useDebugConfigurationContext } from '@/context/debug-configuration'
import type { OnSend } from '@/app/components/base/chat/types'
import type { ChatConfig, OnSend } from '@/app/components/base/chat/types'
import { useEventEmitterContextContext } from '@/context/event-emitter' import { useEventEmitterContextContext } from '@/context/event-emitter'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import { import {
import Avatar from '@/app/components/base/avatar' import Avatar from '@/app/components/base/avatar'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useFeatures } from '@/app/components/base/features/hooks'
import type { InputForm } from '@/app/components/base/chat/chat/type'


type ChatItemProps = { type ChatItemProps = {
modelAndParameter: ModelAndParameter modelAndParameter: ModelAndParameter
modelConfig, modelConfig,
appId, appId,
inputs, inputs,
visionConfig,
collectionList, collectionList,
} = useDebugConfigurationContext() } = useDebugConfigurationContext()
const { textGenerationModelList } = useProviderContext() const { textGenerationModelList } = useProviderContext()
const config = useConfigFromDebugContext()
const features = useFeatures(s => s.features)
const configTemplate = useConfigFromDebugContext()
const config = useMemo(() => {
return {
...configTemplate,
more_like_this: features.moreLikeThis,
opening_statement: features.opening?.enabled ? (features.opening?.opening_statement || '') : '',
suggested_questions: features.opening?.enabled ? (features.opening?.suggested_questions || []) : [],
sensitive_word_avoidance: features.moderation,
speech_to_text: features.speech2text,
text_to_speech: features.text2speech,
file_upload: features.file,
suggested_questions_after_answer: features.suggested,
retriever_resource: features.citation,
annotation_reply: features.annotationReply,
} as ChatConfig
}, [configTemplate, features])
const inputsForm = useMemo(() => {
return modelConfig.configs.prompt_variables.filter(item => item.type !== 'api').map(item => ({ ...item, label: item.name, variable: item.key })) as InputForm[]
}, [modelConfig.configs.prompt_variables])
const { const {
chatList, chatList,
chatListRef, chatListRef,
config, config,
{ {
inputs, inputs,
promptVariables: modelConfig.configs.prompt_variables,
inputsForm,
}, },
[], [],
taskId => stopChatMessageResponding(appId, taskId), taskId => stopChatMessageResponding(appId, taskId),
parent_message_id: chatListRef.current.at(-1)?.id || null, parent_message_id: chatListRef.current.at(-1)?.id || null,
} }


if (visionConfig.enabled && files?.length && supportVision)
if ((config.file_upload as any).enabled && files?.length && supportVision)
data.files = files data.files = files


handleSend( handleSend(
onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController), onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController),
}, },
) )
}, [appId, config, handleSend, inputs, modelAndParameter, textGenerationModelList, visionConfig.enabled, chatListRef])
}, [appId, config, handleSend, inputs, modelAndParameter, textGenerationModelList, chatListRef])


const { eventEmitter } = useEventEmitterContextContext() const { eventEmitter } = useEventEmitterContextContext()
eventEmitter?.useSubscription((v: any) => { eventEmitter?.useSubscription((v: any) => {

+ 28
- 17
web/app/components/app/configuration/debug/debug-with-multiple-model/index.tsx 파일 보기

} from './context' } from './context'
import type { DebugWithMultipleModelContextType } from './context' import type { DebugWithMultipleModelContextType } from './context'
import { useEventEmitterContextContext } from '@/context/event-emitter' import { useEventEmitterContextContext } from '@/context/event-emitter'
import ChatInput from '@/app/components/base/chat/chat/chat-input'
import type { VisionFile } from '@/app/components/base/chat/types'
import ChatInputArea from '@/app/components/base/chat/chat/chat-input-area'
import { useDebugConfigurationContext } from '@/context/debug-configuration' import { useDebugConfigurationContext } from '@/context/debug-configuration'
import { useFeatures } from '@/app/components/base/features/hooks'
import { useStore as useAppStore } from '@/app/components/app/store'
import type { FileEntity } from '@/app/components/base/file-uploader/types'
import type { InputForm } from '@/app/components/base/chat/chat/type'


const DebugWithMultipleModel = () => { const DebugWithMultipleModel = () => {
const { const {
mode, mode,
speechToTextConfig,
visionConfig,
inputs,
modelConfig,
} = useDebugConfigurationContext() } = useDebugConfigurationContext()
const speech2text = useFeatures(s => s.features.speech2text)
const file = useFeatures(s => s.features.file)
const { const {
multipleModelConfigs, multipleModelConfigs,
checkCanSend, checkCanSend,
} = useDebugWithMultipleModelContext() } = useDebugWithMultipleModelContext()

const { eventEmitter } = useEventEmitterContextContext() const { eventEmitter } = useEventEmitterContextContext()
const isChatMode = mode === 'chat' || mode === 'agent-chat' const isChatMode = mode === 'chat' || mode === 'agent-chat'


const handleSend = useCallback((message: string, files?: VisionFile[]) => {
const handleSend = useCallback((message: string, files?: FileEntity[]) => {
if (checkCanSend && !checkCanSend()) if (checkCanSend && !checkCanSend())
return return


} }
}, [twoLine, threeLine, fourLine]) }, [twoLine, threeLine, fourLine])


const setShowAppConfigureFeaturesModal = useAppStore(s => s.setShowAppConfigureFeaturesModal)
const inputsForm = modelConfig.configs.prompt_variables.filter(item => item.type !== 'api').map(item => ({ ...item, label: item.name, variable: item.key })) as InputForm[]

return ( return (
<div className='flex flex-col h-full'> <div className='flex flex-col h-full'>
<div <div
)) ))
} }
</div> </div>
{
isChatMode && (
<div className='shrink-0 pb-4 px-6'>
<ChatInput
onSend={handleSend}
speechToTextConfig={speechToTextConfig}
visionConfig={visionConfig}
noSpacing
/>
</div>
)
}
{isChatMode && (
<div className='shrink-0 pb-0 px-6'>
<ChatInputArea
showFeatureBar
showFileUpload={false}
onFeatureBarClick={setShowAppConfigureFeaturesModal}
onSend={handleSend}
speechToTextConfig={speech2text as any}
visionConfig={file}
inputs={inputs}
inputsForm={inputsForm}
/>
</div>
)}
</div> </div>
) )
} }

+ 10
- 15
web/app/components/app/configuration/debug/debug-with-multiple-model/text-generation-item.tsx 파일 보기

import { TransferMethod } from '@/app/components/base/chat/types' import { TransferMethod } from '@/app/components/base/chat/types'
import { useEventEmitterContextContext } from '@/context/event-emitter' import { useEventEmitterContextContext } from '@/context/event-emitter'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import { useFeatures } from '@/app/components/base/features/hooks'


type TextGenerationItemProps = { type TextGenerationItemProps = {
modelAndParameter: ModelAndParameter modelAndParameter: ModelAndParameter
introduction, introduction,
suggestedQuestionsAfterAnswerConfig, suggestedQuestionsAfterAnswerConfig,
citationConfig, citationConfig,
moderationConfig,
externalDataToolsConfig, externalDataToolsConfig,
chatPromptConfig, chatPromptConfig,
completionPromptConfig, completionPromptConfig,
dataSets, dataSets,
datasetConfigs, datasetConfigs,
visionConfig,
moreLikeThisConfig,
} = useDebugConfigurationContext() } = useDebugConfigurationContext()
const { textGenerationModelList } = useProviderContext() const { textGenerationModelList } = useProviderContext()
const features = useFeatures(s => s.features)
const postDatasets = dataSets.map(({ id }) => ({ const postDatasets = dataSets.map(({ id }) => ({
dataset: { dataset: {
enabled: true, enabled: true,
completion_prompt_config: isAdvancedMode ? completionPromptConfig : {}, completion_prompt_config: isAdvancedMode ? completionPromptConfig : {},
user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables), user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
dataset_query_variable: contextVar || '', dataset_query_variable: contextVar || '',
// features
more_like_this: features.moreLikeThis as any,
sensitive_word_avoidance: features.moderation as any,
text_to_speech: features.text2speech as any,
file_upload: features.file as any,
opening_statement: introduction, opening_statement: introduction,
suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
speech_to_text: speechToTextConfig, speech_to_text: speechToTextConfig,
suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
retriever_resource: citationConfig, retriever_resource: citationConfig,
sensitive_word_avoidance: moderationConfig,
external_data_tools: externalDataToolsConfig, external_data_tools: externalDataToolsConfig,
more_like_this: moreLikeThisConfig,
text_to_speech: {
enabled: false,
voice: '',
language: '',
},
agent_mode: { agent_mode: {
enabled: false, enabled: false,
tools: [], tools: [],
datasets: [...postDatasets], datasets: [...postDatasets],
} as any, } as any,
}, },
file_upload: {
image: visionConfig,
},
} }
const { const {
completion, completion,
model_config: configData, model_config: configData,
} }


if (visionConfig.enabled && files && files?.length > 0) {
if ((config.file_upload as any).enabled && files && files?.length > 0) {
data.files = files.map((item) => { data.files = files.map((item) => {
if (item.transfer_method === TransferMethod.local_file) { if (item.transfer_method === TransferMethod.local_file) {
return { return {
isLoading={!completion && isResponding} isLoading={!completion && isResponding}
isResponding={isResponding} isResponding={isResponding}
isInstalledApp={false} isInstalledApp={false}
siteInfo={null}
messageId={messageId} messageId={messageId}
isError={false} isError={false}
onRetry={() => { }} onRetry={() => { }}

+ 37
- 8
web/app/components/app/configuration/debug/debug-with-single-model/index.tsx 파일 보기

import Chat from '@/app/components/base/chat/chat' import Chat from '@/app/components/base/chat/chat'
import { useChat } from '@/app/components/base/chat/chat/hooks' import { useChat } from '@/app/components/base/chat/chat/hooks'
import { useDebugConfigurationContext } from '@/context/debug-configuration' import { useDebugConfigurationContext } from '@/context/debug-configuration'
import type { ChatItem, OnSend } from '@/app/components/base/chat/types'
import type { ChatConfig, ChatItem, OnSend } from '@/app/components/base/chat/types'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import { import {
fetchConversationMessages, fetchConversationMessages,
import Avatar from '@/app/components/base/avatar' import Avatar from '@/app/components/base/avatar'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useFeatures } from '@/app/components/base/features/hooks'
import { getLastAnswer } from '@/app/components/base/chat/utils' import { getLastAnswer } from '@/app/components/base/chat/utils'
import type { InputForm } from '@/app/components/base/chat/chat/type'


type DebugWithSingleModelProps = { type DebugWithSingleModelProps = {
checkCanSend?: () => boolean checkCanSend?: () => boolean
modelConfig, modelConfig,
appId, appId,
inputs, inputs,
visionConfig,
collectionList, collectionList,
completionParams, completionParams,
// isShowVisionConfig,
} = useDebugConfigurationContext() } = useDebugConfigurationContext()
const { textGenerationModelList } = useProviderContext() const { textGenerationModelList } = useProviderContext()
const config = useConfigFromDebugContext()
const features = useFeatures(s => s.features)
const configTemplate = useConfigFromDebugContext()
const config = useMemo(() => {
return {
...configTemplate,
more_like_this: features.moreLikeThis,
opening_statement: features.opening?.enabled ? (features.opening?.opening_statement || '') : '',
suggested_questions: features.opening?.enabled ? (features.opening?.suggested_questions || []) : [],
sensitive_word_avoidance: features.moderation,
speech_to_text: features.speech2text,
text_to_speech: features.text2speech,
file_upload: features.file,
suggested_questions_after_answer: features.suggested,
retriever_resource: features.citation,
annotation_reply: features.annotationReply,
} as ChatConfig
}, [configTemplate, features])
const inputsForm = useMemo(() => {
return modelConfig.configs.prompt_variables.filter(item => item.type !== 'api').map(item => ({ ...item, label: item.name, variable: item.key })) as InputForm[]
}, [modelConfig.configs.prompt_variables])
const { const {
chatList, chatList,
chatListRef, chatListRef,
config, config,
{ {
inputs, inputs,
promptVariables: modelConfig.configs.prompt_variables,
inputsForm,
}, },
[], [],
taskId => stopChatMessageResponding(appId, taskId), taskId => stopChatMessageResponding(appId, taskId),
parent_message_id: last_answer?.id || getLastAnswer(chatListRef.current)?.id || null, parent_message_id: last_answer?.id || getLastAnswer(chatListRef.current)?.id || null,
} }


if (visionConfig.enabled && files?.length && supportVision)
if ((config.file_upload as any)?.enabled && files?.length && supportVision)
data.files = files data.files = files


handleSend( handleSend(
onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController), onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController),
}, },
) )
}, [chatListRef, appId, checkCanSend, completionParams, config, handleSend, inputs, modelConfig, textGenerationModelList, visionConfig.enabled])
}, [chatListRef, appId, checkCanSend, completionParams, config, handleSend, inputs, modelConfig, textGenerationModelList])


const doRegenerate = useCallback((chatItem: ChatItem) => { const doRegenerate = useCallback((chatItem: ChatItem) => {
const index = chatList.findIndex(item => item.id === chatItem.id) const index = chatList.findIndex(item => item.id === chatItem.id)
} }
}, [handleRestart]) }, [handleRestart])


const setShowAppConfigureFeaturesModal = useAppStore(s => s.setShowAppConfigureFeaturesModal)

return ( return (
<Chat <Chat
config={config} config={config}
chatList={chatList} chatList={chatList}
isResponding={isResponding} isResponding={isResponding}
chatContainerClassName='p-6'
chatFooterClassName='px-6 pt-10 pb-4'
chatContainerClassName='px-3 pt-6'
chatFooterClassName='px-3 pt-10 pb-0'
showFeatureBar
showFileUpload={false}
onFeatureBarClick={setShowAppConfigureFeaturesModal}
suggestedQuestions={suggestedQuestions} suggestedQuestions={suggestedQuestions}
onSend={doSend} onSend={doSend}
inputs={inputs}
inputsForm={inputsForm}
onRegenerate={doRegenerate} onRegenerate={doRegenerate}
onStopResponding={handleStop} onStopResponding={handleStop}
showPromptLog showPromptLog

+ 119
- 86
web/app/components/app/configuration/debug/index.tsx 파일 보기

import useSWR from 'swr' import useSWR from 'swr'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import React, { useCallback, useEffect, useRef, useState } from 'react' import React, { useCallback, useEffect, useRef, useState } from 'react'
import { setAutoFreeze } from 'immer'
import produce, { setAutoFreeze } from 'immer'
import { useBoolean } from 'ahooks' import { useBoolean } from 'ahooks'
import { import {
RiAddLine, RiAddLine,
RiEqualizer2Line,
RiSparklingFill,
} from '@remixicon/react' } from '@remixicon/react'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import { useShallow } from 'zustand/react/shallow' import { useShallow } from 'zustand/react/shallow'
APP_CHAT_WITH_MULTIPLE_MODEL_RESTART, APP_CHAT_WITH_MULTIPLE_MODEL_RESTART,
} from './types' } from './types'
import { AppType, ModelModeType, TransferMethod } from '@/types/app' import { AppType, ModelModeType, TransferMethod } from '@/types/app'
import ChatUserInput from '@/app/components/app/configuration/debug/chat-user-input'
import PromptValuePanel from '@/app/components/app/configuration/prompt-value-panel' import PromptValuePanel from '@/app/components/app/configuration/prompt-value-panel'
import ConfigContext from '@/context/debug-configuration' import ConfigContext from '@/context/debug-configuration'
import { ToastContext } from '@/app/components/base/toast' import { ToastContext } from '@/app/components/base/toast'
import { sendCompletionMessage } from '@/service/debug' import { sendCompletionMessage } from '@/service/debug'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import type { ModelConfig as BackendModelConfig, VisionFile } from '@/types/app'
import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows'
import TooltipPlus from '@/app/components/base/tooltip'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import type { ModelConfig as BackendModelConfig, VisionFile, VisionSettings } from '@/types/app'
import { promptVariablesToUserInputsForm } from '@/utils/model-config' import { promptVariablesToUserInputsForm } from '@/utils/model-config'
import TextGeneration from '@/app/components/app/text-generate/item' import TextGeneration from '@/app/components/app/text-generate/item'
import { IS_CE_EDITION } from '@/config' import { IS_CE_EDITION } from '@/config'
import AgentLogModal from '@/app/components/base/agent-log-modal' import AgentLogModal from '@/app/components/base/agent-log-modal'
import PromptLogModal from '@/app/components/base/prompt-log-modal' import PromptLogModal from '@/app/components/base/prompt-log-modal'
import { useStore as useAppStore } from '@/app/components/app/store' import { useStore as useAppStore } from '@/app/components/app/store'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'


type IDebug = { type IDebug = {
isAPIKeySet: boolean isAPIKeySet: boolean
speechToTextConfig, speechToTextConfig,
textToSpeechConfig, textToSpeechConfig,
citationConfig, citationConfig,
moderationConfig,
moreLikeThisConfig,
formattingChanged, formattingChanged,
setFormattingChanged, setFormattingChanged,
dataSets, dataSets,
completionParams, completionParams,
hasSetContextVar, hasSetContextVar,
datasetConfigs, datasetConfigs,
visionConfig,
setVisionConfig,
} = useContext(ConfigContext) } = useContext(ConfigContext)
const { eventEmitter } = useEventEmitterContextContext() const { eventEmitter } = useEventEmitterContextContext()
const { data: text2speechDefaultModel } = useDefaultModel(ModelTypeEnum.textEmbedding) const { data: text2speechDefaultModel } = useDefaultModel(ModelTypeEnum.textEmbedding)
} }


if (completionFiles.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) { if (completionFiles.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) {
notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') })
notify({ type: 'info', message: t('appDebug.errorMessage.waitForFileUpload') })
return false return false
} }
return !hasEmptyInput return !hasEmptyInput


const [completionRes, setCompletionRes] = useState('') const [completionRes, setCompletionRes] = useState('')
const [messageId, setMessageId] = useState<string | null>(null) const [messageId, setMessageId] = useState<string | null>(null)
const features = useFeatures(s => s.features)
const featuresStore = useFeaturesStore()


const sendTextCompletion = async () => { const sendTextCompletion = async () => {
if (isResponding) { if (isResponding) {
completion_prompt_config: {}, completion_prompt_config: {},
user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables), user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
dataset_query_variable: contextVar || '', dataset_query_variable: contextVar || '',
opening_statement: introduction,
suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
speech_to_text: speechToTextConfig,
retriever_resource: citationConfig,
sensitive_word_avoidance: moderationConfig,
more_like_this: moreLikeThisConfig,
model: {
provider: modelConfig.provider,
name: modelConfig.model_id,
mode: modelConfig.mode,
completion_params: completionParams as any,
},
text_to_speech: {
enabled: false,
voice: '',
language: '',
},
agent_mode: {
enabled: false,
tools: [],
},
dataset_configs: { dataset_configs: {
...datasetConfigs, ...datasetConfigs,
datasets: { datasets: {
datasets: [...postDatasets], datasets: [...postDatasets],
} as any, } as any,
}, },
file_upload: {
image: visionConfig,
agent_mode: {
enabled: false,
tools: [],
},
model: {
provider: modelConfig.provider,
name: modelConfig.model_id,
mode: modelConfig.mode,
completion_params: completionParams as any,
}, },
more_like_this: features.moreLikeThis as any,
sensitive_word_avoidance: features.moderation as any,
text_to_speech: features.text2speech as any,
file_upload: features.file as any,
opening_statement: introduction,
suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
speech_to_text: speechToTextConfig,
retriever_resource: citationConfig,
} }


if (isAdvancedMode) { if (isAdvancedMode) {
model_config: postModelConfig, model_config: postModelConfig,
} }


if (visionConfig.enabled && completionFiles && completionFiles?.length > 0) {
if ((features.file as any).enabled && completionFiles && completionFiles?.length > 0) {
data.files = completionFiles.map((item) => { data.files = completionFiles.map((item) => {
if (item.transfer_method === TransferMethod.local_file) { if (item.transfer_method === TransferMethod.local_file) {
return { return {
) )
} }


const handleVisionConfigInMultipleModel = () => {
const handleVisionConfigInMultipleModel = useCallback(() => {
if (debugWithMultipleModel && mode) { if (debugWithMultipleModel && mode) {
const supportedVision = multipleModelConfigs.some((modelConfig) => { const supportedVision = multipleModelConfigs.some((modelConfig) => {
const currentProvider = textGenerationModelList.find(modelItem => modelItem.provider === modelConfig.provider) const currentProvider = textGenerationModelList.find(modelItem => modelItem.provider === modelConfig.provider)


return currentModel?.features?.includes(ModelFeatureEnum.vision) return currentModel?.features?.includes(ModelFeatureEnum.vision)
}) })

if (supportedVision) {
setVisionConfig({
...visionConfig,
enabled: true,
}, true)
}
else {
setVisionConfig({
...visionConfig,
enabled: false,
}, true)
}
const {
features,
setFeatures,
} = featuresStore!.getState()

const newFeatures = produce(features, (draft) => {
draft.file = {
...draft.file,
enabled: supportedVision,
}
})
setFeatures(newFeatures)
} }
}
}, [debugWithMultipleModel, featuresStore, mode, multipleModelConfigs, textGenerationModelList])


useEffect(() => { useEffect(() => {
handleVisionConfigInMultipleModel() handleVisionConfigInMultipleModel()
}, [multipleModelConfigs, mode])
}, [multipleModelConfigs, mode, handleVisionConfigInMultipleModel])


const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showAgentLogModal, setShowAgentLogModal } = useAppStore(useShallow(state => ({ const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showAgentLogModal, setShowAgentLogModal } = useAppStore(useShallow(state => ({
currentLogItem: state.currentLogItem, currentLogItem: state.currentLogItem,
adjustModalWidth() adjustModalWidth()
}, []) }, [])


const [expanded, setExpanded] = useState(true)

return ( return (
<> <>
<div className="shrink-0 pt-4 px-6">
<div className='flex items-center justify-between mb-2'>
<div className='h2 '>{t('appDebug.inputs.title')}</div>
<div className="shrink-0">
<div className='flex items-center justify-between px-4 pt-3 pb-2'>
<div className='text-text-primary system-xl-semibold'>{t('appDebug.inputs.title')}</div>
<div className='flex items-center'> <div className='flex items-center'>
{ {
debugWithMultipleModel debugWithMultipleModel
? ( ? (
<> <>
<Button <Button
variant='secondary-accent'
variant='ghost-accent'
onClick={() => onMultipleModelConfigsChange(true, [...multipleModelConfigs, { id: `${Date.now()}`, model: '', provider: '', parameters: {} }])} onClick={() => onMultipleModelConfigsChange(true, [...multipleModelConfigs, { id: `${Date.now()}`, model: '', provider: '', parameters: {} }])}
disabled={multipleModelConfigs.length >= 4} disabled={multipleModelConfigs.length >= 4}
> >
<RiAddLine className='mr-1 w-3.5 h-3.5' /> <RiAddLine className='mr-1 w-3.5 h-3.5' />
{t('common.modelProvider.addModel')}({multipleModelConfigs.length}/4) {t('common.modelProvider.addModel')}({multipleModelConfigs.length}/4)
</Button> </Button>
<div className='mx-2 w-[1px] h-[14px] bg-gray-200' />
<div className='mx-2 w-[1px] h-[14px] bg-divider-regular' />
</> </>
) )
: null : null
} }
{mode !== AppType.completion && ( {mode !== AppType.completion && (
<Button variant='secondary-accent' className='gap-1' onClick={clearConversation}>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.66663 2.66629V5.99963H3.05463M3.05463 5.99963C3.49719 4.90505 4.29041 3.98823 5.30998 3.39287C6.32954 2.7975 7.51783 2.55724 8.68861 2.70972C9.85938 2.8622 10.9465 3.39882 11.7795 4.23548C12.6126 5.07213 13.1445 6.16154 13.292 7.33296M3.05463 5.99963H5.99996M13.3333 13.333V9.99963H12.946M12.946 9.99963C12.5028 11.0936 11.7093 12.0097 10.6898 12.6045C9.67038 13.1993 8.48245 13.4393 7.31203 13.2869C6.1416 13.1344 5.05476 12.5982 4.22165 11.7621C3.38854 10.926 2.8562 9.83726 2.70796 8.66629M12.946 9.99963H9.99996" stroke="#1C64F2" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
<span className='text-primary-600 text-[13px] font-semibold'>{t('common.operation.refresh')}</span>
</Button>
<>
<TooltipPlus
popupContent={t('common.operation.refresh')}
>
<ActionButton onClick={clearConversation}>
<RefreshCcw01 className='w-4 h-4' />
</ActionButton>
</TooltipPlus>
{varList.length > 0 && (
<div className='relative ml-1 mr-2'>
<TooltipPlus
popupContent={t('workflow.panel.userInputField')}
>
<ActionButton state={expanded ? ActionButtonState.Active : undefined} onClick={() => setExpanded(!expanded)}>
<RiEqualizer2Line className='w-4 h-4' />
</ActionButton>
</TooltipPlus>
{expanded && <div className='absolute z-10 bottom-[-14px] right-[5px] w-3 h-3 bg-components-panel-on-panel-item-bg border-l-[0.5px] border-t-[0.5px] border-components-panel-border-subtle rotate-45' />}
</div>
)}
</>
)} )}
</div> </div>
</div> </div>
<PromptValuePanel
appType={mode as AppType}
onSend={handleSendTextCompletion}
inputs={inputs}
visionConfig={{
...visionConfig,
image_file_size_limit: fileUploadConfigResponse?.image_file_size_limit,
}}
onVisionFilesChange={setCompletionFiles}
/>
{mode !== AppType.completion && expanded && (
<div className='mx-3'>
<ChatUserInput inputs={inputs} />
</div>
)}
{mode === AppType.completion && (
<PromptValuePanel
appType={mode as AppType}
onSend={handleSendTextCompletion}
inputs={inputs}
visionConfig={{
...features.file! as VisionSettings,
transfer_methods: features.file!.allowed_file_upload_methods || [],
image_file_size_limit: fileUploadConfigResponse?.image_file_size_limit,
}}
onVisionFilesChange={setCompletionFiles}
/>
)}
</div> </div>
{ {
debugWithMultipleModel && ( debugWithMultipleModel && (
)} )}
{/* Text Generation */} {/* Text Generation */}
{mode === AppType.completion && ( {mode === AppType.completion && (
<div className="mt-6 px-6 pb-4">
<GroupName name={t('appDebug.result')} />
<>
{(completionRes || isResponding) && ( {(completionRes || isResponding) && (
<TextGeneration
className="mt-2"
content={completionRes}
isLoading={!completionRes && isResponding}
isShowTextToSpeech={textToSpeechConfig.enabled && !!text2speechDefaultModel}
isResponding={isResponding}
isInstalledApp={false}
messageId={messageId}
isError={false}
onRetry={() => { }}
supportAnnotation
appId={appId}
varList={varList}
siteInfo={null}
/>
<>
<div className='mx-4 mt-3'><GroupName name={t('appDebug.result')} /></div>
<div className='mx-3 mb-8'>
<TextGeneration
className="mt-2"
content={completionRes}
isLoading={!completionRes && isResponding}
isShowTextToSpeech={textToSpeechConfig.enabled && !!text2speechDefaultModel}
isResponding={isResponding}
isInstalledApp={false}
messageId={messageId}
isError={false}
onRetry={() => { }}
supportAnnotation
appId={appId}
varList={varList}
siteInfo={null}
/>
</div>
</>
)} )}
</div>
{!completionRes && !isResponding && (
<div className='grow flex flex-col items-center justify-center gap-2'>
<RiSparklingFill className='w-12 h-12 text-text-empty-state-icon' />
<div className='text-text-quaternary system-sm-regular'>{t('appDebug.noResult')}</div>
</div>
)}
</>
)} )}
{mode === AppType.completion && showPromptLogModal && ( {mode === AppType.completion && showPromptLogModal && (
<PromptLogModal <PromptLogModal

+ 0
- 25
web/app/components/app/configuration/features/chat-group/citation/index.tsx 파일 보기

'use client'
import React, { type FC } from 'react'
import { useTranslation } from 'react-i18next'
import Panel from '@/app/components/app/configuration/base/feature-panel'
import { Citations } from '@/app/components/base/icons/src/vender/solid/editor'

const Citation: FC = () => {
const { t } = useTranslation()

return (
<Panel
title={
<div className='flex items-center gap-2'>
<div>{t('appDebug.feature.citation.title')}</div>
</div>
}
headerIcon={<Citations className='w-4 h-4 text-[#FD853A]' />}
headerRight={
<div className='text-xs text-gray-500'>{t('appDebug.feature.citation.resDes')}</div>
}
noBodySpacing
/>
)
}
export default React.memo(Citation)

+ 0
- 65
web/app/components/app/configuration/features/chat-group/index.tsx 파일 보기

'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import GroupName from '../../base/group-name'
import type { IOpeningStatementProps } from './opening-statement'
import OpeningStatement from './opening-statement'
import SuggestedQuestionsAfterAnswer from './suggested-questions-after-answer'
import SpeechToText from './speech-to-text'
import TextToSpeech from './text-to-speech'
import Citation from './citation'
/*
* Include
* 1. Conversation Opener
* 2. Opening Suggestion
* 3. Next question suggestion
*/
type ChatGroupProps = {
isShowOpeningStatement: boolean
openingStatementConfig: IOpeningStatementProps
isShowSuggestedQuestionsAfterAnswer: boolean
isShowSpeechText: boolean
isShowTextToSpeech: boolean
isShowCitation: boolean
}
const ChatGroup: FC<ChatGroupProps> = ({
isShowOpeningStatement,
openingStatementConfig,
isShowSuggestedQuestionsAfterAnswer,
isShowSpeechText,
isShowTextToSpeech,
isShowCitation,
}) => {
const { t } = useTranslation()

return (
<div className='mt-7'>
<GroupName name={t('appDebug.feature.groupChat.title')} />
<div className='space-y-3'>
{isShowOpeningStatement && (
<OpeningStatement {...openingStatementConfig} />
)}
{isShowSuggestedQuestionsAfterAnswer && (
<SuggestedQuestionsAfterAnswer />
)}
{
isShowTextToSpeech && (
<TextToSpeech />
)
}
{
isShowSpeechText && (
<SpeechToText />
)
}
{
isShowCitation && (
<Citation />
)
}
</div>
</div>
)
}
export default React.memo(ChatGroup)

+ 0
- 25
web/app/components/app/configuration/features/chat-group/speech-to-text/index.tsx 파일 보기

'use client'
import React, { type FC } from 'react'
import { useTranslation } from 'react-i18next'
import Panel from '@/app/components/app/configuration/base/feature-panel'
import { Microphone01 } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'

const SpeechToTextConfig: FC = () => {
const { t } = useTranslation()

return (
<Panel
title={
<div className='flex items-center gap-2'>
<div>{t('appDebug.feature.speechToText.title')}</div>
</div>
}
headerIcon={<Microphone01 className='w-4 h-4 text-[#7839EE]' />}
headerRight={
<div className='text-xs text-gray-500'>{t('appDebug.feature.speechToText.resDes')}</div>
}
noBodySpacing
/>
)
}
export default React.memo(SpeechToTextConfig)

+ 0
- 34
web/app/components/app/configuration/features/chat-group/suggested-questions-after-answer/index.tsx 파일 보기

'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import Panel from '@/app/components/app/configuration/base/feature-panel'
import SuggestedQuestionsAfterAnswerIcon from '@/app/components/app/configuration/base/icons/suggested-questions-after-answer-icon'
import Tooltip from '@/app/components/base/tooltip'

const SuggestedQuestionsAfterAnswer: FC = () => {
const { t } = useTranslation()

return (
<Panel
title={
<div className='flex items-center gap-1'>
<div>{t('appDebug.feature.suggestedQuestionsAfterAnswer.title')}</div>
<Tooltip
popupContent={
<div className='w-[180px]'>
{t('appDebug.feature.suggestedQuestionsAfterAnswer.description')}
</div>
}
/>
</div>
}
headerIcon={<SuggestedQuestionsAfterAnswerIcon />}
headerRight={
<div className='text-xs text-gray-500'>{t('appDebug.feature.suggestedQuestionsAfterAnswer.resDes')}</div>
}
noBodySpacing
/>
)
}
export default React.memo(SuggestedQuestionsAfterAnswer)

+ 0
- 55
web/app/components/app/configuration/features/chat-group/text-to-speech/index.tsx 파일 보기

'use client'
import useSWR from 'swr'
import React, { type FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { usePathname } from 'next/navigation'
import Panel from '@/app/components/app/configuration/base/feature-panel'
import { Speaker } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
import ConfigContext from '@/context/debug-configuration'
import { languages } from '@/i18n/language'
import { fetchAppVoices } from '@/service/apps'
import AudioBtn from '@/app/components/base/audio-btn'

const TextToSpeech: FC = () => {
const { t } = useTranslation()
const {
textToSpeechConfig,
} = useContext(ConfigContext)

const pathname = usePathname()
const matched = pathname.match(/\/app\/([^/]+)/)
const appId = (matched?.length && matched[1]) ? matched[1] : ''
const language = textToSpeechConfig.language
const languageInfo = languages.find(i => i.value === textToSpeechConfig.language)

const voiceItems = useSWR({ appId, language }, fetchAppVoices).data
const voiceItem = voiceItems?.find(item => item.value === textToSpeechConfig.voice)

return (
<Panel
title={
<div className='flex items-center'>
<div>{t('appDebug.feature.textToSpeech.title')}</div>
</div>
}
headerIcon={<Speaker className='w-4 h-4 text-[#7839EE]' />}
headerRight={
<div className='text-xs text-gray-500 inline-flex items-center gap-2'>
{languageInfo && (`${languageInfo?.name} - `)}{voiceItem?.name ?? t('appDebug.voice.defaultDisplay')}
{ languageInfo?.example && (
<AudioBtn
value={languageInfo?.example}
isAudition
voice={textToSpeechConfig.voice}
noCache
/>
)}
</div>
}
noBodySpacing
isShowTextToSpeech
/>
)
}
export default React.memo(TextToSpeech)

+ 229
- 174
web/app/components/app/configuration/index.tsx 파일 보기

'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React, { useEffect, useMemo, useRef, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import { usePathname } from 'next/navigation' import { usePathname } from 'next/navigation'
import { clone, isEqual } from 'lodash-es' import { clone, isEqual } from 'lodash-es'
import { CodeBracketIcon } from '@heroicons/react/20/solid' import { CodeBracketIcon } from '@heroicons/react/20/solid'
import { useShallow } from 'zustand/react/shallow' import { useShallow } from 'zustand/react/shallow'
import Button from '../../base/button'
import Loading from '../../base/loading'
import AppPublisher from '../app-publisher'
import AgentSettingButton from './config/agent-setting-button'
import useAdvancedPromptConfig from './hooks/use-advanced-prompt-config'
import EditHistoryModal from './config-prompt/conversation-history/edit-modal'
import AgentSettingButton from '@/app/components/app/configuration/config/agent-setting-button'
import useAdvancedPromptConfig from '@/app/components/app/configuration/hooks/use-advanced-prompt-config'
import EditHistoryModal from '@/app/components/app/configuration/config-prompt/conversation-history/edit-modal'
import { import {
useDebugWithSingleOrMultipleModel, useDebugWithSingleOrMultipleModel,
useFormattingChangedDispatcher, useFormattingChangedDispatcher,
} from './debug/hooks'
import type { ModelAndParameter } from './debug/types'
} from '@/app/components/app/configuration/debug/hooks'
import type { ModelAndParameter } from '@/app/components/app/configuration/debug/types'
import Button from '@/app/components/base/button'
import Loading from '@/app/components/base/loading'
import AppPublisher from '@/app/components/app/app-publisher/features-wrapper'
import type { import type {
AnnotationReplyConfig, AnnotationReplyConfig,
DatasetConfigs, DatasetConfigs,
getMultipleRetrievalConfig, getMultipleRetrievalConfig,
getSelectedDatasetsMode, getSelectedDatasetsMode,
} from '@/app/components/workflow/nodes/knowledge-retrieval/utils' } from '@/app/components/workflow/nodes/knowledge-retrieval/utils'
import { FeaturesProvider } from '@/app/components/base/features'
import type { Features as FeaturesData, FileUpload } from '@/app/components/base/features/types'
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
import NewFeaturePanel from '@/app/components/base/features/new-feature-panel'


type PublishConfig = { type PublishConfig = {
modelConfig: ModelConfig modelConfig: ModelConfig
const Configuration: FC = () => { const Configuration: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { notify } = useContext(ToastContext) const { notify } = useContext(ToastContext)
const { appDetail, setAppSiderbarExpand } = useAppStore(useShallow(state => ({
const { appDetail, showAppConfigureFeaturesModal, setAppSiderbarExpand, setShowAppConfigureFeaturesModal } = useAppStore(useShallow(state => ({
appDetail: state.appDetail, appDetail: state.appDetail,
setAppSiderbarExpand: state.setAppSiderbarExpand, setAppSiderbarExpand: state.setAppSiderbarExpand,
showAppConfigureFeaturesModal: state.showAppConfigureFeaturesModal,
setShowAppConfigureFeaturesModal: state.setShowAppConfigureFeaturesModal,
}))) })))
const latestPublishedAt = useMemo(() => appDetail?.model_config.updated_at, [appDetail])
const [formattingChanged, setFormattingChanged] = useState(false) const [formattingChanged, setFormattingChanged] = useState(false)
const { setShowAccountSettingModal } = useModalContext() const { setShowAccountSettingModal } = useModalContext()
const [hasFetchedDetail, setHasFetchedDetail] = useState(false) const [hasFetchedDetail, setHasFetchedDetail] = useState(false)
const [mode, setMode] = useState('') const [mode, setMode] = useState('')
const [publishedConfig, setPublishedConfig] = useState<PublishConfig | null>(null) const [publishedConfig, setPublishedConfig] = useState<PublishConfig | null>(null)


const modalConfig = useMemo(() => appDetail?.model_config || {} as BackendModelConfig, [appDetail])
const [conversationId, setConversationId] = useState<string | null>('') const [conversationId, setConversationId] = useState<string | null>('')


const media = useBreakpoints() const media = useBreakpoints()
prompt_template: '', prompt_template: '',
prompt_variables: [] as PromptVariable[], prompt_variables: [] as PromptVariable[],
}, },
opening_statement: '',
more_like_this: null, more_like_this: null,
suggested_questions_after_answer: null,
opening_statement: '',
suggested_questions: [],
sensitive_word_avoidance: null,
speech_to_text: null, speech_to_text: null,
text_to_speech: null, text_to_speech: null,
file_upload: null,
suggested_questions_after_answer: null,
retriever_resource: null, retriever_resource: null,
sensitive_word_avoidance: null,
annotation_reply: null,
dataSets: [], dataSets: [],
agentConfig: DEFAULT_AGENT_SETTING, agentConfig: DEFAULT_AGENT_SETTING,
}) })
setModelConfig(_publishedConfig.modelConfig) setModelConfig(_publishedConfig.modelConfig)
setCompletionParams(_publishedConfig.completionParams) setCompletionParams(_publishedConfig.completionParams)
setDataSets(modelConfig.dataSets || []) setDataSets(modelConfig.dataSets || [])
// feature
// reset feature
setIntroduction(modelConfig.opening_statement!) setIntroduction(modelConfig.opening_statement!)
setMoreLikeThisConfig(modelConfig.more_like_this || { setMoreLikeThisConfig(modelConfig.more_like_this || {
enabled: false, enabled: false,


const isShowVisionConfig = !!currModel?.features?.includes(ModelFeatureEnum.vision) const isShowVisionConfig = !!currModel?.features?.includes(ModelFeatureEnum.vision)


// *** web app features ***
const featuresData: FeaturesData = useMemo(() => {
return {
moreLikeThis: modelConfig.more_like_this || { enabled: false },
opening: {
enabled: !!modelConfig.opening_statement,
opening_statement: modelConfig.opening_statement || '',
suggested_questions: modelConfig.suggested_questions || [],
},
moderation: modelConfig.sensitive_word_avoidance || { enabled: false },
speech2text: modelConfig.speech_to_text || { enabled: false },
text2speech: modelConfig.text_to_speech || { enabled: false },
file: {
image: {
detail: modelConfig.file_upload?.image?.detail || Resolution.high,
enabled: !!modelConfig.file_upload?.image?.enabled,
number_limits: modelConfig.file_upload?.image?.number_limits || 3,
transfer_methods: modelConfig.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
},
enabled: !!(modelConfig.file_upload?.enabled || modelConfig.file_upload?.image?.enabled),
allowed_file_types: modelConfig.file_upload?.allowed_file_types || [SupportUploadFileTypes.image],
allowed_file_extensions: modelConfig.file_upload?.allowed_file_extensions || FILE_EXTS[SupportUploadFileTypes.image].map(ext => `.${ext}`),
allowed_file_upload_methods: modelConfig.file_upload?.allowed_file_upload_methods || modelConfig.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
number_limits: modelConfig.file_upload?.number_limits || modelConfig.file_upload?.image?.number_limits || 3,
} as FileUpload,
suggested: modelConfig.suggested_questions_after_answer || { enabled: false },
citation: modelConfig.retriever_resource || { enabled: false },
annotationReply: modelConfig.annotation_reply || { enabled: false },
}
}, [modelConfig])
const handleFeaturesChange = useCallback((flag: any) => {
setShowAppConfigureFeaturesModal(true)
if (flag)
formattingChangedDispatcher()
}, [formattingChangedDispatcher, setShowAppConfigureFeaturesModal])
const handleAddPromptVariable = useCallback((variable: PromptVariable[]) => {
const newModelConfig = produce(modelConfig, (draft: ModelConfig) => {
draft.configs.prompt_variables = variable
})
setModelConfig(newModelConfig)
}, [modelConfig])

useEffect(() => { useEffect(() => {
(async () => { (async () => {
const collectionList = await fetchCollectionList() const collectionList = await fetchCollectionList()
modelConfig.dataset_query_variable, modelConfig.dataset_query_variable,
), ),
}, },
opening_statement: modelConfig.opening_statement,
more_like_this: modelConfig.more_like_this, more_like_this: modelConfig.more_like_this,
suggested_questions_after_answer: modelConfig.suggested_questions_after_answer,
opening_statement: modelConfig.opening_statement,
suggested_questions: modelConfig.suggested_questions,
sensitive_word_avoidance: modelConfig.sensitive_word_avoidance,
speech_to_text: modelConfig.speech_to_text, speech_to_text: modelConfig.speech_to_text,
text_to_speech: modelConfig.text_to_speech, text_to_speech: modelConfig.text_to_speech,
file_upload: modelConfig.file_upload,
suggested_questions_after_answer: modelConfig.suggested_questions_after_answer,
retriever_resource: modelConfig.retriever_resource, retriever_resource: modelConfig.retriever_resource,
sensitive_word_avoidance: modelConfig.sensitive_word_avoidance,
annotation_reply: modelConfig.annotation_reply,
external_data_tools: modelConfig.external_data_tools, external_data_tools: modelConfig.external_data_tools,
dataSets: datasets || [], dataSets: datasets || [],
// eslint-disable-next-line multiline-ternary // eslint-disable-next-line multiline-ternary
else { return promptEmpty } else { return promptEmpty }
})() })()
const contextVarEmpty = mode === AppType.completion && dataSets.length > 0 && !hasSetContextVar const contextVarEmpty = mode === AppType.completion && dataSets.length > 0 && !hasSetContextVar
const onPublish = async (modelAndParameter?: ModelAndParameter) => {
const onPublish = async (modelAndParameter?: ModelAndParameter, features?: FeaturesData) => {
const modelId = modelAndParameter?.model || modelConfig.model_id const modelId = modelAndParameter?.model || modelConfig.model_id
const promptTemplate = modelConfig.configs.prompt_template const promptTemplate = modelConfig.configs.prompt_template
const promptVariables = modelConfig.configs.prompt_variables const promptVariables = modelConfig.configs.prompt_variables
completion_prompt_config: {}, completion_prompt_config: {},
user_input_form: promptVariablesToUserInputsForm(promptVariables), user_input_form: promptVariablesToUserInputsForm(promptVariables),
dataset_query_variable: contextVar || '', dataset_query_variable: contextVar || '',
opening_statement: introduction || '',
suggested_questions: suggestedQuestions || [],
more_like_this: moreLikeThisConfig,
suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
speech_to_text: speechToTextConfig,
text_to_speech: textToSpeechConfig,
retriever_resource: citationConfig,
sensitive_word_avoidance: moderationConfig,
// features
more_like_this: features?.moreLikeThis as any,
opening_statement: features?.opening?.enabled ? (features.opening?.opening_statement || '') : '',
suggested_questions: features?.opening?.enabled ? (features.opening?.suggested_questions || []) : [],
sensitive_word_avoidance: features?.moderation as any,
speech_to_text: features?.speech2text as any,
text_to_speech: features?.text2speech as any,
file_upload: features?.file as any,
suggested_questions_after_answer: features?.suggested as any,
retriever_resource: features?.citation as any,
agent_mode: { agent_mode: {
...modelConfig.agentConfig, ...modelConfig.agentConfig,
strategy: isFunctionCall ? AgentStrategy.functionCall : AgentStrategy.react, strategy: isFunctionCall ? AgentStrategy.functionCall : AgentStrategy.react,
datasets: [...postDatasets], datasets: [...postDatasets],
} as any, } as any,
}, },
file_upload: {
image: visionConfig,
},
} }


if (isAdvancedMode) { if (isAdvancedMode) {
return true return true
} }


const [restoreConfirmOpen, setRestoreConfirmOpen] = useState(false)
const resetAppConfig = () => {
syncToPublishedConfig(publishedConfig!)
setRestoreConfirmOpen(false)
}

const [showUseGPT4Confirm, setShowUseGPT4Confirm] = useState(false) const [showUseGPT4Confirm, setShowUseGPT4Confirm] = useState(false)


const { const {
setRerankSettingModalOpen, setRerankSettingModalOpen,
}} }}
> >
<>
<div className="flex flex-col h-full">
<div className='relative flex grow h-[200px] pt-14'>
{/* Header */}
<div className='absolute top-0 left-0 w-full bg-white h-14'>
<div className='flex items-center justify-between px-6 h-14'>
<div className='flex items-center'>
<div className='text-base font-semibold leading-6 text-gray-900'>{t('appDebug.orchestrate')}</div>
<div className='flex items-center h-[14px] space-x-1 text-xs'>
{isAdvancedMode && (
<div className='ml-1 flex items-center h-5 px-1.5 border border-gray-100 rounded-md text-[11px] font-medium text-gray-500 uppercase'>{t('appDebug.promptMode.advanced')}</div>
)}
<FeaturesProvider features={featuresData}>
<>
<div className="flex flex-col h-full">
<div className='relative flex grow h-[200px] pt-14'>
{/* Header */}
<div className='absolute top-0 left-0 w-full bg-white h-14'>
<div className='flex items-center justify-between px-6 h-14'>
<div className='flex items-center'>
<div className='text-base font-semibold leading-6 text-gray-900'>{t('appDebug.orchestrate')}</div>
<div className='flex items-center h-[14px] space-x-1 text-xs'>
{isAdvancedMode && (
<div className='ml-1 flex items-center h-5 px-1.5 border border-gray-100 rounded-md text-[11px] font-medium text-gray-500 uppercase'>{t('appDebug.promptMode.advanced')}</div>
)}
</div>
</div> </div>
</div>
<div className='flex items-center'>
{/* Agent Setting */}
{isAgent && (
<AgentSettingButton
isChatModel={modelConfig.mode === ModelModeType.chat}
agentConfig={modelConfig.agentConfig}

isFunctionCall={isFunctionCall}
onAgentSettingChange={(config) => {
const nextConfig = produce(modelConfig, (draft: ModelConfig) => {
draft.agentConfig = config
})
setModelConfig(nextConfig)
}}
/>
)}
{/* Model and Parameters */}
{!debugWithMultipleModel && (
<>
<ModelParameterModal
isAdvancedMode={isAdvancedMode}
mode={mode}
provider={modelConfig.provider}
completionParams={completionParams}
modelId={modelConfig.model_id}
setModel={setModel as any}
onCompletionParamsChange={(newParams: FormValue) => {
setCompletionParams(newParams)
<div className='flex items-center'>
{/* Agent Setting */}
{isAgent && (
<AgentSettingButton
isChatModel={modelConfig.mode === ModelModeType.chat}
agentConfig={modelConfig.agentConfig}

isFunctionCall={isFunctionCall}
onAgentSettingChange={(config) => {
const nextConfig = produce(modelConfig, (draft: ModelConfig) => {
draft.agentConfig = config
})
setModelConfig(nextConfig)
}} }}
debugWithMultipleModel={debugWithMultipleModel}
onDebugWithMultipleModelChange={handleDebugWithMultipleModelChange}
/> />
<div className='mx-2 w-[1px] h-[14px] bg-gray-200'></div>
</>
)}
{isMobile && (
<Button className='!h-8 !text-[13px] font-medium' onClick={showDebugPanel}>
<span className='mr-1'>{t('appDebug.operation.debugConfig')}</span>
<CodeBracketIcon className="w-4 h-4 text-gray-500" />
</Button>
)}
<AppPublisher {...{
publishDisabled: cannotPublish,
publishedAt: (modalConfig.created_at || 0) * 1000,
debugWithMultipleModel,
multipleModelConfigs,
onPublish,
onRestore: () => setRestoreConfirmOpen(true),
}} />
)}
{/* Model and Parameters */}
{!debugWithMultipleModel && (
<>
<ModelParameterModal
isAdvancedMode={isAdvancedMode}
mode={mode}
provider={modelConfig.provider}
completionParams={completionParams}
modelId={modelConfig.model_id}
setModel={setModel as any}
onCompletionParamsChange={(newParams: FormValue) => {
setCompletionParams(newParams)
}}
debugWithMultipleModel={debugWithMultipleModel}
onDebugWithMultipleModelChange={handleDebugWithMultipleModelChange}
/>
<div className='mx-2 w-[1px] h-[14px] bg-gray-200'></div>
</>
)}
{isMobile && (
<Button className='!h-8 !text-[13px] font-medium' onClick={showDebugPanel}>
<span className='mr-1'>{t('appDebug.operation.debugConfig')}</span>
<CodeBracketIcon className="w-4 h-4 text-gray-500" />
</Button>
)}
<AppPublisher {...{
publishDisabled: cannotPublish,
publishedAt: (latestPublishedAt || 0) * 1000,
debugWithMultipleModel,
multipleModelConfigs,
onPublish,
publishedConfig: publishedConfig!,
resetAppConfig: () => syncToPublishedConfig(publishedConfig!),
}} />
</div>
</div> </div>
</div> </div>
</div>
<div className={`w-full sm:w-1/2 shrink-0 flex flex-col h-full ${debugWithMultipleModel && 'max-w-[560px]'}`}>
<Config />
</div>
{!isMobile && <div className="relative flex flex-col w-1/2 h-full overflow-y-auto grow " style={{ borderColor: 'rgba(0, 0, 0, 0.02)' }}>
<div className='flex flex-col h-0 border-t border-l grow rounded-tl-2xl bg-gray-50 '>
<Debug
isAPIKeySet={isAPIKeySet}
onSetting={() => setShowAccountSettingModal({ payload: 'provider' })}
inputs={inputs}
modelParameterParams={{
setModel: setModel as any,
onCompletionParamsChange: setCompletionParams,
}}
debugWithMultipleModel={debugWithMultipleModel}
multipleModelConfigs={multipleModelConfigs}
onMultipleModelConfigsChange={handleMultipleModelConfigsChange}
/>
<div className={`w-full sm:w-1/2 shrink-0 flex flex-col h-full ${debugWithMultipleModel && 'max-w-[560px]'}`}>
<Config />
</div> </div>
</div>}
{!isMobile && <div className="relative flex flex-col w-1/2 h-full overflow-y-auto grow " style={{ borderColor: 'rgba(0, 0, 0, 0.02)' }}>
<div className='grow flex flex-col border-t-[0.5px] border-l-[0.5px] rounded-tl-2xl border-components-panel-border bg-chatbot-bg '>
<Debug
isAPIKeySet={isAPIKeySet}
onSetting={() => setShowAccountSettingModal({ payload: 'provider' })}
inputs={inputs}
modelParameterParams={{
setModel: setModel as any,
onCompletionParamsChange: setCompletionParams,
}}
debugWithMultipleModel={debugWithMultipleModel}
multipleModelConfigs={multipleModelConfigs}
onMultipleModelConfigsChange={handleMultipleModelConfigsChange}
/>
</div>
</div>}
</div>
</div> </div>
</div>
{restoreConfirmOpen && (
<Confirm
title={t('appDebug.resetConfig.title')}
content={t('appDebug.resetConfig.message')}
isShow={restoreConfirmOpen}
onConfirm={resetAppConfig}
onCancel={() => setRestoreConfirmOpen(false)}
/>
)}
{showUseGPT4Confirm && (
<Confirm
title={t('appDebug.trailUseGPT4Info.title')}
content={t('appDebug.trailUseGPT4Info.description')}
isShow={showUseGPT4Confirm}
onConfirm={() => {
setShowAccountSettingModal({ payload: 'provider' })
setShowUseGPT4Confirm(false)
}}
onCancel={() => setShowUseGPT4Confirm(false)}
/>
)}

{isShowSelectDataSet && (
<SelectDataSet
isShow={isShowSelectDataSet}
onClose={hideSelectDataSet}
selectedIds={selectedIds}
onSelect={handleSelect}
/>
)}

{isShowHistoryModal && (
<EditHistoryModal
isShow={isShowHistoryModal}
saveLoading={false}
onClose={hideHistoryModal}
data={completionPromptConfig.conversation_histories_role}
onSave={(data) => {
setConversationHistoriesRole(data)
hideHistoryModal()
}}
/>
)}
{isMobile && (
<Drawer showClose isOpen={isShowDebugPanel} onClose={hideDebugPanel} mask footer={null} panelClassname='!bg-gray-50'>
<Debug
isAPIKeySet={isAPIKeySet}
onSetting={() => setShowAccountSettingModal({ payload: 'provider' })}
inputs={inputs}
modelParameterParams={{
setModel: setModel as any,
onCompletionParamsChange: setCompletionParams,
{showUseGPT4Confirm && (
<Confirm
title={t('appDebug.trailUseGPT4Info.title')}
content={t('appDebug.trailUseGPT4Info.description')}
isShow={showUseGPT4Confirm}
onConfirm={() => {
setShowAccountSettingModal({ payload: 'provider' })
setShowUseGPT4Confirm(false)
}} }}
debugWithMultipleModel={debugWithMultipleModel}
multipleModelConfigs={multipleModelConfigs}
onMultipleModelConfigsChange={handleMultipleModelConfigsChange}
onCancel={() => setShowUseGPT4Confirm(false)}
/>
)}

{isShowSelectDataSet && (
<SelectDataSet
isShow={isShowSelectDataSet}
onClose={hideSelectDataSet}
selectedIds={selectedIds}
onSelect={handleSelect}
/>
)}

{isShowHistoryModal && (
<EditHistoryModal
isShow={isShowHistoryModal}
saveLoading={false}
onClose={hideHistoryModal}
data={completionPromptConfig.conversation_histories_role}
onSave={(data) => {
setConversationHistoriesRole(data)
hideHistoryModal()
}}
/>
)}
{isMobile && (
<Drawer showClose isOpen={isShowDebugPanel} onClose={hideDebugPanel} mask footer={null} panelClassname='!bg-gray-50'>
<Debug
isAPIKeySet={isAPIKeySet}
onSetting={() => setShowAccountSettingModal({ payload: 'provider' })}
inputs={inputs}
modelParameterParams={{
setModel: setModel as any,
onCompletionParamsChange: setCompletionParams,
}}
debugWithMultipleModel={debugWithMultipleModel}
multipleModelConfigs={multipleModelConfigs}
onMultipleModelConfigsChange={handleMultipleModelConfigsChange}
/>
</Drawer>
)}
{showAppConfigureFeaturesModal && (
<NewFeaturePanel
show
inWorkflow={false}
showFileUpload={false}
isChatMode={mode !== 'completion'}
disabled={false}
onChange={handleFeaturesChange}
onClose={() => setShowAppConfigureFeaturesModal(false)}
promptVariables={modelConfig.configs.prompt_variables}
onAutoAddPromptVariable={handleAddPromptVariable}
/> />
</Drawer>
)}
</>
)}
</>
</FeaturesProvider>
</ConfigContext.Provider> </ConfigContext.Provider>
) )
} }

+ 126
- 164
web/app/components/app/configuration/prompt-value-panel/index.tsx 파일 보기

'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React, { useState } from 'react'
import React, { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import { import {
RiArrowDownSLine, RiArrowDownSLine,
RiArrowRightLine,
RiArrowRightSLine,
RiPlayLargeFill,
} from '@remixicon/react' } from '@remixicon/react'
import {
PlayIcon,
} from '@heroicons/react/24/solid'
import ConfigContext from '@/context/debug-configuration' import ConfigContext from '@/context/debug-configuration'
import type { Inputs, PromptVariable } from '@/models/debug'
import type { Inputs } from '@/models/debug'
import { AppType, ModelModeType } from '@/types/app' import { AppType, ModelModeType } from '@/types/app'
import Select from '@/app/components/base/select' import Select from '@/app/components/base/select'
import { DEFAULT_VALUE_MAX_LEN } from '@/config'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import TextGenerationImageUploader from '@/app/components/base/image-uploader/text-generation-image-uploader' import TextGenerationImageUploader from '@/app/components/base/image-uploader/text-generation-image-uploader'
import FeatureBar from '@/app/components/base/features/new-feature-panel/feature-bar'
import type { VisionFile, VisionSettings } from '@/types/app' import type { VisionFile, VisionSettings } from '@/types/app'
import { DEFAULT_VALUE_MAX_LEN } from '@/config'
import { useStore as useAppStore } from '@/app/components/app/store'
import cn from '@/utils/classnames'


export type IPromptValuePanelProps = { export type IPromptValuePanelProps = {
appType: AppType appType: AppType
return key && key?.trim() && name && name?.trim() return key && key?.trim() && name && name?.trim()
}) })


const promptVariableObj = (() => {
const promptVariableObj = useMemo(() => {
const obj: Record<string, boolean> = {} const obj: Record<string, boolean> = {}
promptVariables.forEach((input) => { promptVariables.forEach((input) => {
obj[input.key] = true obj[input.key] = true
}) })
return obj return obj
})()
}, [promptVariables])


const canNotRun = (() => {
const canNotRun = useMemo(() => {
if (mode !== AppType.completion) if (mode !== AppType.completion)
return true return true


} }


else { return !modelConfig.configs.prompt_template } else { return !modelConfig.configs.prompt_template }
})()
const renderRunButton = () => {
return (
<Button
variant="primary"
disabled={canNotRun}
onClick={() => onSend && onSend()}
className="w-[80px] !h-8">
<PlayIcon className="shrink-0 w-4 h-4 mr-1" aria-hidden="true" />
<span className='uppercase text-[13px]'>{t('appDebug.inputs.run')}</span>
</Button>
)
}
}, [chatPromptConfig.prompt, completionPromptConfig.prompt?.text, isAdvancedMode, mode, modelConfig.configs.prompt_template, modelModeType])

const handleInputValueChange = (key: string, value: string) => { const handleInputValueChange = (key: string, value: string) => {
if (!(key in promptVariableObj)) if (!(key in promptVariableObj))
return return
setInputs(newInputs) setInputs(newInputs)
} }


const setShowAppConfigureFeaturesModal = useAppStore(s => s.setShowAppConfigureFeaturesModal)

return ( return (
<div className="pb-3 border border-gray-200 bg-white rounded-xl" style={{
boxShadow: '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)',
}}>
<div className={'mt-3 px-4 bg-white'}>
<div className={
`${!userInputFieldCollapse && 'mb-2'}`
}>
<div className='flex items-center space-x-1 cursor-pointer' onClick={() => setUserInputFieldCollapse(!userInputFieldCollapse)}>
{
userInputFieldCollapse
? <RiArrowRightLine className='w-3 h-3 text-gray-300' />
: <RiArrowDownSLine className='w-3 h-3 text-gray-300' />
}
<div className='text-xs font-medium text-gray-800 uppercase'>{t('appDebug.inputs.userInputField')}</div>
<>
<div className='relative z-[1] mx-3 border-[0.5px] bg-components-panel-on-panel-item-bg border-components-panel-border-subtle rounded-xl shadow-md'>
<div className={cn('px-4 pt-3', userInputFieldCollapse ? 'pb-3' : 'pb-1')}>
<div className='flex items-center gap-0.5 py-0.5 cursor-pointer' onClick={() => setUserInputFieldCollapse(!userInputFieldCollapse)}>
<div className='text-text-secondary system-md-semibold-uppercase'>{t('appDebug.inputs.userInputField')}</div>
{userInputFieldCollapse && <RiArrowRightSLine className='w-4 h-4 text-text-secondary'/>}
{!userInputFieldCollapse && <RiArrowDownSLine className='w-4 h-4 text-text-secondary'/>}
</div> </div>
{appType === AppType.completion && promptVariables.length > 0 && !userInputFieldCollapse && (
<div className="mt-1 text-xs leading-normal text-gray-500">{t('appDebug.inputs.completionVarTip')}</div>
{!userInputFieldCollapse && (
<div className='mt-1 text-text-tertiary system-xs-regular'>{t('appDebug.inputs.completionVarTip')}</div>
)} )}
</div> </div>
{!userInputFieldCollapse && (
<>
{
promptVariables.length > 0
? (
<div className="space-y-3 ">
{promptVariables.map(({ key, name, type, options, max_length, required }) => (
<div key={key} className="xl:flex justify-between">
<div className="mr-1 py-2 shrink-0 w-[120px] text-sm text-gray-900">{name || key}</div>
{type === 'select' && (
<Select
className='w-full'
defaultValue={inputs[key] as string}
onSelect={(i) => { handleInputValueChange(key, i.value as string) }}
items={(options || []).map(i => ({ name: i, value: i }))}
allowSearch={false}
bgClassName='bg-gray-50'
/>
)
}
{type === 'string' && (
<input
className="w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
placeholder={`${name}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
type="text"
value={inputs[key] ? `${inputs[key]}` : ''}
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
/>
)}
{type === 'paragraph' && (
<textarea
className="w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-[120px] bg-gray-50 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
placeholder={`${name}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
value={inputs[key] ? `${inputs[key]}` : ''}
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
/>
)}
{type === 'number' && (
<input
className="w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
placeholder={`${name}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
type="number"
value={inputs[key] ? `${inputs[key]}` : ''}
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
/>
)}
</div>
))}
{!userInputFieldCollapse && promptVariables.length > 0 && (
<div className='px-4 pt-3 pb-4'>
{promptVariables.map(({ key, name, type, options, max_length, required }, index) => (
<div
key={key}
className='mb-4 last-of-type:mb-0'
>
<div>
<div className='h-6 mb-1 flex items-center gap-1 text-text-secondary system-sm-semibold'>
<div className='truncate'>{name || key}</div>
{!required && <span className='text-text-tertiary system-xs-regular'>{t('workflow.panel.optional')}</span>}
</div> </div>
)
: (
<div className='text-xs text-gray-500'>{t('appDebug.inputs.noVar')}</div>
)
}
{
appType === AppType.completion && visionConfig?.enabled && (
<div className="mt-3 xl:flex justify-between">
<div className="mr-1 py-2 shrink-0 w-[120px] text-sm text-gray-900">{t('common.imageUploader.imageUpload')}</div>
<div className='grow'> <div className='grow'>
<TextGenerationImageUploader
settings={visionConfig}
onFilesChange={files => onVisionFilesChange(files.filter(file => file.progress !== -1).map(fileItem => ({
type: 'image',
transfer_method: fileItem.type,
url: fileItem.url,
upload_file_id: fileItem.fileId,
})))}
/>
{type === 'string' && (
<Input
value={inputs[key] ? `${inputs[key]}` : ''}
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
placeholder={name}
autoFocus={index === 0}
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
/>
)}
{type === 'paragraph' && (
<Textarea
className='grow h-[120px]'
placeholder={name}
value={inputs[key] ? `${inputs[key]}` : ''}
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
/>
)}
{type === 'select' && (
<Select
className='w-full'
defaultValue={inputs[key] as string}
onSelect={(i) => { handleInputValueChange(key, i.value as string) }}
items={(options || []).map(i => ({ name: i, value: i }))}
allowSearch={false}
bgClassName='bg-gray-50'
/>
)}
{type === 'number' && (
<Input
type='number'
value={inputs[key] ? `${inputs[key]}` : ''}
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
placeholder={name}
autoFocus={index === 0}
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
/>
)}
</div> </div>
</div> </div>
)
}
</>
)
}
</div>

{
appType === AppType.completion && (
<div>
<div className="mt-5 border-b border-gray-100"></div>
<div className="flex justify-between mt-4 px-4">
</div>
))}
{visionConfig?.enabled && (
<div className="mt-3 xl:flex justify-between">
<div className="mr-1 py-2 shrink-0 w-[120px] text-sm text-gray-900">{t('common.imageUploader.imageUpload')}</div>
<div className='grow'>
<TextGenerationImageUploader
settings={visionConfig}
onFilesChange={files => onVisionFilesChange(files.filter(file => file.progress !== -1).map(fileItem => ({
type: 'image',
transfer_method: fileItem.type,
url: fileItem.url,
upload_file_id: fileItem.fileId,
})))}
/>
</div>
</div>
)}
</div>
)}
{!userInputFieldCollapse && (
<div className='flex justify-between p-4 pt-3 border-t border-divider-subtle'>
<Button className='w-[72px]' onClick={onClear}>{t('common.operation.clear')}</Button>
{canNotRun && (
<Tooltip popupContent={t('appDebug.otherError.promptNoBeEmpty')} needsDelay>
<Button
variant="primary"
disabled={canNotRun}
onClick={() => onSend && onSend()}
className="w-[96px]">
<RiPlayLargeFill className="shrink-0 w-4 h-4 mr-0.5" aria-hidden="true" />
{t('appDebug.inputs.run')}
</Button>
</Tooltip>
)}
{!canNotRun && (
<Button <Button
onClick={onClear}
disabled={false}
>
<span className='text-[13px]'>{t('common.operation.clear')}</span>
variant="primary"
disabled={canNotRun}
onClick={() => onSend && onSend()}
className="w-[96px]">
<RiPlayLargeFill className="shrink-0 w-4 h-4 mr-0.5" aria-hidden="true" />
{t('appDebug.inputs.run')}
</Button> </Button>

{canNotRun
? (<Tooltip
popupContent={t('appDebug.otherError.promptNoBeEmpty')}
needsDelay
>
{renderRunButton()}
</Tooltip>)
: renderRunButton()}
</div>
)}
</div> </div>
)
}
</div>
)}
</div>
<div className='mx-3'>
<FeatureBar
showFileUpload={false}
isChatMode={appType !== AppType.completion}
onFeatureBarClick={setShowAppConfigureFeaturesModal} />
</div>
</>
) )
} }


export default React.memo(PromptValuePanel) export default React.memo(PromptValuePanel)

function replaceStringWithValuesWithFormat(str: string, promptVariables: PromptVariable[], inputs: Record<string, any>) {
return str.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
const name = inputs[key]
if (name) { // has set value
return `<div class='inline-block px-1 rounded-md text-gray-900' style='background: rgba(16, 24, 40, 0.1)'>${name}</div>`
}

const valueObj: PromptVariable | undefined = promptVariables.find(v => v.key === key)
return `<div class='inline-block px-1 rounded-md text-gray-500' style='background: rgba(16, 24, 40, 0.05)'>${valueObj ? valueObj.name : match}</div>`
})
}

export function replaceStringWithValues(str: string, promptVariables: PromptVariable[], inputs: Record<string, any>) {
return str.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
const name = inputs[key]
if (name) { // has set value
return name
}

const valueObj: PromptVariable | undefined = promptVariables.find(v => v.key === key)
return valueObj ? `{{${valueObj.name}}}` : match
})
}

// \n -> br
function format(str: string) {
return str.replaceAll('\n', '<br>')
}

+ 13
- 0
web/app/components/app/configuration/prompt-value-panel/utils.ts 파일 보기

import type { PromptVariable } from '@/models/debug'

export function replaceStringWithValues(str: string, promptVariables: PromptVariable[], inputs: Record<string, any>) {
return str.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
const name = inputs[key]
if (name) { // has set value
return name
}

const valueObj: PromptVariable | undefined = promptVariables.find(v => v.key === key)
return valueObj ? `{{${valueObj.name}}}` : match
})
}

+ 0
- 80
web/app/components/app/configuration/toolbox/moderation/index.tsx 파일 보기

import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import { useContext } from 'use-context-selector'
import { FileSearch02 } from '@/app/components/base/icons/src/vender/solid/files'
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
import { useModalContext } from '@/context/modal-context'
import ConfigContext from '@/context/debug-configuration'
import { fetchCodeBasedExtensionList } from '@/service/common'
import I18n from '@/context/i18n'
const Moderation = () => {
const { t } = useTranslation()
const { setShowModerationSettingModal } = useModalContext()
const { locale } = useContext(I18n)
const {
moderationConfig,
setModerationConfig,
} = useContext(ConfigContext)
const { data: codeBasedExtensionList } = useSWR(
'/code-based-extension?module=moderation',
fetchCodeBasedExtensionList,
)

const handleOpenModerationSettingModal = () => {
setShowModerationSettingModal({
payload: moderationConfig,
onSaveCallback: setModerationConfig,
})
}

const renderInfo = () => {
let prefix = ''
let suffix = ''
if (moderationConfig.type === 'openai_moderation')
prefix = t('appDebug.feature.moderation.modal.provider.openai')
else if (moderationConfig.type === 'keywords')
prefix = t('appDebug.feature.moderation.modal.provider.keywords')
else if (moderationConfig.type === 'api')
prefix = t('common.apiBasedExtension.selector.title')
else
prefix = codeBasedExtensionList?.data.find(item => item.name === moderationConfig.type)?.label[locale] || ''

if (moderationConfig.config?.inputs_config?.enabled && moderationConfig.config?.outputs_config?.enabled)
suffix = t('appDebug.feature.moderation.allEnabled')
else if (moderationConfig.config?.inputs_config?.enabled)
suffix = t('appDebug.feature.moderation.inputEnabled')
else if (moderationConfig.config?.outputs_config?.enabled)
suffix = t('appDebug.feature.moderation.outputEnabled')

return `${prefix} · ${suffix}`
}

return (
<div className='flex items-center px-3 h-12 bg-gray-50 rounded-xl overflow-hidden'>
<div className='shrink-0 flex items-center justify-center mr-1 w-6 h-6'>
<FileSearch02 className='shrink-0 w-4 h-4 text-[#039855]' />
</div>
<div className='shrink-0 mr-2 whitespace-nowrap text-sm text-gray-800 font-semibold'>
{t('appDebug.feature.moderation.title')}
</div>
<div
className='grow block w-0 text-right text-xs text-gray-500 truncate'
title={renderInfo()}>
{renderInfo()}
</div>
<div className='shrink-0 ml-4 mr-1 w-[1px] h-3.5 bg-gray-200'></div>
<div
className={`
shrink-0 flex items-center px-3 h-7 cursor-pointer rounded-md
text-xs text-gray-700 font-medium hover:bg-gray-200
`}
onClick={handleOpenModerationSettingModal}
>
<Settings01 className='mr-[5px] w-3.5 h-3.5' />
{t('common.operation.settings')}
</div>
</div>
)
}

export default Moderation

+ 1
- 1
web/app/components/app/configuration/tools/external-data-tool-modal.tsx 파일 보기

import useSWR from 'swr' import useSWR from 'swr'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import FormGeneration from '../toolbox/moderation/form-generation'
import FormGeneration from '@/app/components/base/features/new-feature-panel/moderation/form-generation'
import Modal from '@/app/components/base/modal' import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import EmojiPicker from '@/app/components/base/emoji-picker' import EmojiPicker from '@/app/components/base/emoji-picker'

+ 6
- 4
web/app/components/app/create-app-modal/index.tsx 파일 보기

import { createApp } from '@/service/apps' import { createApp } from '@/service/apps'
import Modal from '@/app/components/base/modal' import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import AppIcon from '@/app/components/base/app-icon' import AppIcon from '@/app/components/base/app-icon'
import AppsFull from '@/app/components/billing/apps-full-in-dialog' import AppsFull from '@/app/components/billing/apps-full-in-dialog'
import { AiText, ChatBot, CuteRobot } from '@/app/components/base/icons/src/vender/solid/communication' import { AiText, ChatBot, CuteRobot } from '@/app/components/base/icons/src/vender/solid/communication'
size='large' className='cursor-pointer' size='large' className='cursor-pointer'
onClick={() => { setShowAppIconPicker(true) }} onClick={() => { setShowAppIconPicker(true) }}
/> />
<input
<Input
value={name} value={name}
onChange={e => setName(e.target.value)} onChange={e => setName(e.target.value)}
placeholder={t('app.newApp.appNamePlaceholder') || ''} placeholder={t('app.newApp.appNamePlaceholder') || ''}
className='grow h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg border border-transparent outline-none appearance-none caret-primary-600 placeholder:text-gray-400 hover:bg-gray-50 hover:border hover:border-gray-300 focus:bg-gray-50 focus:border focus:border-gray-300 focus:shadow-xs'
className='grow h-10'
/> />
</div> </div>
{showAppIconPicker && <AppIconPicker {showAppIconPicker && <AppIconPicker
{/* description */} {/* description */}
<div className='pt-2 px-8'> <div className='pt-2 px-8'>
<div className='py-2 text-sm font-medium leading-[20px] text-gray-900'>{t('app.newApp.captionDescription')}</div> <div className='py-2 text-sm font-medium leading-[20px] text-gray-900'>{t('app.newApp.captionDescription')}</div>
<textarea
className='w-full px-3 py-2 text-sm font-normal bg-gray-100 rounded-lg border border-transparent outline-none appearance-none caret-primary-600 placeholder:text-gray-400 hover:bg-gray-50 hover:border hover:border-gray-300 focus:bg-gray-50 focus:border focus:border-gray-300 focus:shadow-xs h-[80px] resize-none'
<Textarea
className='resize-none'
placeholder={t('app.newApp.appDescriptionPlaceholder') || ''} placeholder={t('app.newApp.appDescriptionPlaceholder') || ''}
value={description} value={description}
onChange={e => setDescription(e.target.value)} onChange={e => setDescription(e.target.value)}

+ 2
- 2
web/app/components/app/create-from-dsl-modal/index.tsx 파일 보기

import { RiCloseLine } from '@remixicon/react' import { RiCloseLine } from '@remixicon/react'
import Uploader from './uploader' import Uploader from './uploader'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal' import Modal from '@/app/components/base/modal'
import { ToastContext } from '@/app/components/base/toast' import { ToastContext } from '@/app/components/base/toast'
import { import {
currentTab === CreateFromDSLModalTab.FROM_URL && ( currentTab === CreateFromDSLModalTab.FROM_URL && (
<div> <div>
<div className='mb-1 system-md-semibold leading6'>DSL URL</div> <div className='mb-1 system-md-semibold leading6'>DSL URL</div>
<input
<Input
placeholder={t('app.importFromDSLUrlPlaceholder') || ''} placeholder={t('app.importFromDSLUrlPlaceholder') || ''}
className='px-2 w-full h-8 border border-components-input-border-active bg-components-input-bg-active rounded-lg outline-none appearance-none placeholder:text-components-input-text-placeholder system-sm-regular'
value={dslUrlValue} value={dslUrlValue}
onChange={e => setDslUrlValue(e.target.value)} onChange={e => setDslUrlValue(e.target.value)}
/> />

+ 3
- 2
web/app/components/app/duplicate-modal/index.tsx 파일 보기

import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import Modal from '@/app/components/base/modal' import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import AppIcon from '@/app/components/base/app-icon' import AppIcon from '@/app/components/base/app-icon'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
background={appIcon.type === 'image' ? undefined : appIcon.background} background={appIcon.type === 'image' ? undefined : appIcon.background}
imageUrl={appIcon.type === 'image' ? appIcon.url : undefined} imageUrl={appIcon.type === 'image' ? appIcon.url : undefined}
/> />
<input
<Input
value={name} value={name}
onChange={e => setName(e.target.value)} onChange={e => setName(e.target.value)}
className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow'
className='h-10'
/> />
</div> </div>
{isAppsFull && <AppsFull loc='app-duplicate-create' />} {isAppsFull && <AppsFull loc='app-duplicate-create' />}

+ 2
- 2
web/app/components/app/log-annotation/index.tsx 파일 보기

import WorkflowLog from '@/app/components/app/workflow-log' import WorkflowLog from '@/app/components/app/workflow-log'
import Annotation from '@/app/components/app/annotation' import Annotation from '@/app/components/app/annotation'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import { PageType } from '@/app/components/app/configuration/toolbox/annotation/type'
import { PageType } from '@/app/components/base/features/new-feature-panel/annotation-reply/type'
import TabSlider from '@/app/components/base/tab-slider-plain' import TabSlider from '@/app/components/base/tab-slider-plain'
import { useStore as useAppStore } from '@/app/components/app/store' import { useStore as useAppStore } from '@/app/components/app/store'


} }


return ( return (
<div className='pt-4 px-6 h-full flex flex-col'>
<div className='pt-3 px-6 h-full flex flex-col'>
{appDetail.mode !== 'workflow' && ( {appDetail.mode !== 'workflow' && (
<TabSlider <TabSlider
className='shrink-0' className='shrink-0'

+ 38
- 40
web/app/components/app/log/filter.tsx 파일 보기

import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import {
MagnifyingGlassIcon,
} from '@heroicons/react/24/solid'
import useSWR from 'swr' import useSWR from 'swr'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { RiCalendarLine } from '@remixicon/react'
import quarterOfYear from 'dayjs/plugin/quarterOfYear' import quarterOfYear from 'dayjs/plugin/quarterOfYear'
import type { QueryParam } from './index' import type { QueryParam } from './index'
import { SimpleSelect } from '@/app/components/base/select'
import Chip from '@/app/components/base/chip'
import Input from '@/app/components/base/input'
import Sort from '@/app/components/base/sort' import Sort from '@/app/components/base/sort'
import { fetchAnnotationsCount } from '@/service/log' import { fetchAnnotationsCount } from '@/service/log'
dayjs.extend(quarterOfYear) dayjs.extend(quarterOfYear)
if (!data) if (!data)
return null return null
return ( return (
<div className='flex flex-row flex-wrap gap-2 items-center mb-4 text-gray-900 text-base'>
<SimpleSelect
<div className='flex flex-row flex-wrap gap-2 items-center mb-2'>
<Chip
className='min-w-[150px]'
panelClassName='w-[270px]'
leftIcon={<RiCalendarLine className='h-4 w-4 text-text-secondary' />}
value={queryParams.period || 7}
onSelect={(item) => {
setQueryParams({ ...queryParams, period: item.value as string })
}}
onClear={() => setQueryParams({ ...queryParams, period: 7 })}
items={TIME_PERIOD_LIST.map(item => ({ value: item.value, name: t(`appLog.filter.period.${item.name}`) }))} items={TIME_PERIOD_LIST.map(item => ({ value: item.value, name: t(`appLog.filter.period.${item.name}`) }))}
className='mt-0 !w-40'
/>
<Chip
className='min-w-[150px]'
panelClassName='w-[270px]'
showLeftIcon={false}
value={queryParams.annotation_status || 'all'}
onSelect={(item) => { onSelect={(item) => {
setQueryParams({ ...queryParams, period: item.value })
setQueryParams({ ...queryParams, annotation_status: item.value as string })
}}
onClear={() => setQueryParams({ ...queryParams, annotation_status: 'all' })}
items={[
{ value: 'all', name: t('appLog.filter.annotation.all') },
{ value: 'annotated', name: t('appLog.filter.annotation.annotated', { count: data?.count }) },
{ value: 'not_annotated', name: t('appLog.filter.annotation.not_annotated') },
]}
/>
<Input
wrapperClassName='w-[200px]'
showLeftIcon
showClearIcon
value={queryParams.keyword}
placeholder={t('common.operation.search')!}
onChange={(e) => {
setQueryParams({ ...queryParams, keyword: e.target.value })
}} }}
defaultValue={queryParams.period} />
<div className="relative rounded-md">
<SimpleSelect
defaultValue={'all'}
className='!w-[300px]'
onSelect={
(item) => {
if (!item.value)
return
setQueryParams({ ...queryParams, annotation_status: item.value as string })
}
}
items={[{ value: 'all', name: t('appLog.filter.annotation.all') },
{ value: 'annotated', name: t('appLog.filter.annotation.annotated', { count: data?.count }) },
{ value: 'not_annotated', name: t('appLog.filter.annotation.not_annotated') }]}
/>
</div>
<div className="relative">
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<MagnifyingGlassIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div>
<input
type="text"
name="query"
className="block w-[180px] bg-gray-100 shadow-sm rounded-md border-0 py-1.5 pl-10 text-gray-900 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-gray-200 focus-visible:outline-none sm:text-sm sm:leading-6"
placeholder={t('common.operation.search')!}
value={queryParams.keyword}
onChange={(e) => {
setQueryParams({ ...queryParams, keyword: e.target.value })
}}
/>
</div>
onClear={() => setQueryParams({ ...queryParams, keyword: '' })}
/>
{isChatMode && ( {isChatMode && (
<> <>
<div className='w-px h-3.5 bg-divider-regular'></div> <div className='w-px h-3.5 bg-divider-regular'></div>

+ 5
- 5
web/app/components/app/log/index.tsx 파일 보기

const pathSegments = pathname.split('/') const pathSegments = pathname.split('/')
pathSegments.pop() pathSegments.pop()
return <div className='flex items-center justify-center h-full'> return <div className='flex items-center justify-center h-full'>
<div className='bg-gray-50 w-[560px] h-fit box-border px-5 py-4 rounded-2xl'>
<span className='text-gray-700 font-semibold'>{t('appLog.table.empty.element.title')}<ThreeDotsIcon className='inline relative -top-3 -left-1.5' /></span>
<div className='mt-2 text-gray-500 text-sm font-normal'>
<div className='bg-background-section-burn w-[560px] h-fit box-border px-5 py-4 rounded-2xl'>
<span className='text-text-secondary system-md-semibold'>{t('appLog.table.empty.element.title')}<ThreeDotsIcon className='inline relative -top-3 -left-1.5' /></span>
<div className='mt-2 text-text-tertiary system-sm-regular'>
<Trans <Trans
i18nKey="appLog.table.empty.element.content" i18nKey="appLog.table.empty.element.content"
components={{ shareLink: <Link href={`${pathSegments.join('/')}/overview`} className='text-primary-600' />, testLink: <Link href={appUrl} className='text-primary-600' target='_blank' rel='noopener noreferrer' /> }}
components={{ shareLink: <Link href={`${pathSegments.join('/')}/overview`} className='text-util-colors-blue-blue-600' />, testLink: <Link href={appUrl} className='text-util-colors-blue-blue-600' target='_blank' rel='noopener noreferrer' /> }}
/> />
</div> </div>
</div> </div>


return ( return (
<div className='flex flex-col h-full'> <div className='flex flex-col h-full'>
<p className='flex text-sm font-normal text-gray-500'>{t('appLog.description')}</p>
<p className='text-text-tertiary system-sm-regular'>{t('appLog.description')}</p>
<div className='flex flex-col py-4 flex-1'> <div className='flex flex-col py-4 flex-1'>
<Filter isChatMode={isChatMode} appId={appDetail.id} queryParams={queryParams} setQueryParams={setQueryParams} /> <Filter isChatMode={isChatMode} appId={appDetail.id} queryParams={queryParams} setQueryParams={setQueryParams} />
{total === undefined {total === undefined

+ 38
- 30
web/app/components/app/log/list.tsx 파일 보기

import { useShallow } from 'zustand/react/shallow' import { useShallow } from 'zustand/react/shallow'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { UUID_NIL } from '../../base/chat/constants' import { UUID_NIL } from '../../base/chat/constants'
import s from './style.module.css'
import VarPanel from './var-panel' import VarPanel from './var-panel'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import type { FeedbackFunc, FeedbackType, IChatItem, SubmitAnnotationFunc } from '@/app/components/base/chat/chat/type' import type { FeedbackFunc, FeedbackType, IChatItem, SubmitAnnotationFunc } from '@/app/components/base/chat/chat/type'
import type { Annotation, ChatConversationFullDetailResponse, ChatConversationGeneralDetail, ChatConversationsResponse, ChatMessage, ChatMessagesRequest, CompletionConversationFullDetailResponse, CompletionConversationGeneralDetail, CompletionConversationsResponse, LogAnnotation } from '@/models/log'
import type { Annotation, ChatConversationGeneralDetail, ChatConversationsResponse, ChatMessage, ChatMessagesRequest, CompletionConversationGeneralDetail, CompletionConversationsResponse, LogAnnotation } from '@/models/log'
import type { App } from '@/types/app' import type { App } from '@/types/app'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import Drawer from '@/app/components/base/drawer' import Drawer from '@/app/components/base/drawer'
import useTimestamp from '@/hooks/use-timestamp' import useTimestamp from '@/hooks/use-timestamp'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import { CopyIcon } from '@/app/components/base/copy-icon' import { CopyIcon } from '@/app/components/base/copy-icon'
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'


dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
} }


function appendQAToChatList(newChatList: IChatItem[], item: any, conversationId: string, timezone: string, format: string) { function appendQAToChatList(newChatList: IChatItem[], item: any, conversationId: string, timezone: string, format: string) {
const answerFiles = item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || []
newChatList.push({ newChatList.push({
id: item.id, id: item.id,
content: item.answer, content: item.answer,
adminFeedback: item.feedbacks.find((item: any) => item.from_source === 'admin'), // admin feedback adminFeedback: item.feedbacks.find((item: any) => item.from_source === 'admin'), // admin feedback
feedbackDisabled: false, feedbackDisabled: false,
isAnswer: true, isAnswer: true,
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
message_files: getProcessedFilesFromResponse(answerFiles.map((item: any) => ({ ...item, related_id: item.id }))),
log: [ log: [
...item.message, ...item.message,
...(item.message[item.message.length - 1]?.role !== 'assistant' ...(item.message[item.message.length - 1]?.role !== 'assistant'
})(), })(),
parentMessageId: `question-${item.id}`, parentMessageId: `question-${item.id}`,
}) })
const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || []
newChatList.push({ newChatList.push({
id: `question-${item.id}`, id: `question-${item.id}`,
content: item.inputs.query || item.inputs.default_input || item.query, // text generation: item.inputs.query; chat: item.query content: item.inputs.query || item.inputs.default_input || item.query, // text generation: item.inputs.query; chat: item.query
isAnswer: false, isAnswer: false,
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
message_files: getProcessedFilesFromResponse(questionFiles.map((item: any) => ({ ...item, related_id: item.id }))),
parentMessageId: item.parent_message_id || undefined, parentMessageId: item.parent_message_id || undefined,
}) })
} }
// const displayedParams = CompletionParams.slice(0, -2) // const displayedParams = CompletionParams.slice(0, -2)
const validatedParams = ['temperature', 'top_p', 'presence_penalty', 'frequency_penalty'] const validatedParams = ['temperature', 'top_p', 'presence_penalty', 'frequency_penalty']


type IDetailPanel<T> = {
type IDetailPanel = {
detail: any detail: any
onFeedback: FeedbackFunc onFeedback: FeedbackFunc
onSubmitAnnotation: SubmitAnnotationFunc onSubmitAnnotation: SubmitAnnotationFunc
} }


function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionConversationFullDetailResponse>({ detail, onFeedback }: IDetailPanel<T>) {
function DetailPanel({ detail, onFeedback }: IDetailPanel) {
const { userProfile: { timezone } } = useAppContext() const { userProfile: { timezone } } = useAppContext()
const { formatTime } = useTimestamp() const { formatTime } = useTimestamp()
const { onClose, appDetail } = useContext(DrawerContext) const { onClose, appDetail } = useContext(DrawerContext)
if (!conversationDetail) if (!conversationDetail)
return null return null


return <DetailPanel<CompletionConversationFullDetailResponse>
return <DetailPanel
detail={conversationDetail} detail={conversationDetail}
onFeedback={handleFeedback} onFeedback={handleFeedback}
onSubmitAnnotation={handleAnnotation} onSubmitAnnotation={handleAnnotation}
if (!conversationDetail) if (!conversationDetail)
return null return null


return <DetailPanel<ChatConversationFullDetailResponse>
return <DetailPanel
detail={conversationDetail} detail={conversationDetail}
onFeedback={handleFeedback} onFeedback={handleFeedback}
onSubmitAnnotation={handleAnnotation} onSubmitAnnotation={handleAnnotation}
return ( return (
<Tooltip <Tooltip
popupContent={ popupContent={
<span className='text-xs text-gray-500 inline-flex items-center'>
<span className='text-xs text-text-tertiary inline-flex items-center'>
<RiEditFill className='w-3 h-3 mr-1' />{`${t('appLog.detail.annotationTip', { user: annotation?.account?.name })} ${formatTime(annotation?.created_at || dayjs().unix(), 'MM-DD hh:mm A')}`} <RiEditFill className='w-3 h-3 mr-1' />{`${t('appLog.detail.annotationTip', { user: annotation?.account?.name })} ${formatTime(annotation?.created_at || dayjs().unix(), 'MM-DD hh:mm A')}`}
</span> </span>
} }
popupClassName={(isHighlight && !isChatMode) ? '' : '!hidden'} popupClassName={(isHighlight && !isChatMode) ? '' : '!hidden'}
> >
<div className={cn(isEmptyStyle ? 'text-gray-400' : 'text-gray-700', !isHighlight ? '' : 'bg-orange-100', 'text-sm overflow-hidden text-ellipsis whitespace-nowrap')}>
<div className={cn(isEmptyStyle ? 'text-text-quaternary' : 'text-text-secondary', !isHighlight ? '' : 'bg-orange-100', 'system-sm-regular overflow-hidden text-ellipsis whitespace-nowrap')}>
{value || '-'} {value || '-'}
</div> </div>
</Tooltip> </Tooltip>


return ( return (
<div className='overflow-x-auto'> <div className='overflow-x-auto'>
<table className={`w-full min-w-[440px] border-collapse border-0 text-sm mt-3 ${s.logTable}`}>
<thead className="h-8 leading-8 border-b border-gray-200 text-gray-500 font-bold">
<table className={cn('mt-2 w-full min-w-[440px] border-collapse border-0')}>
<thead className='system-xs-medium-uppercase text-text-tertiary'>
<tr> <tr>
<td className='w-[1.375rem] whitespace-nowrap'></td>
<td className='whitespace-nowrap'>{isChatMode ? t('appLog.table.header.summary') : t('appLog.table.header.input')}</td>
<td className='whitespace-nowrap'>{t('appLog.table.header.endUser')}</td>
<td className='whitespace-nowrap'>{isChatMode ? t('appLog.table.header.messageCount') : t('appLog.table.header.output')}</td>
<td className='whitespace-nowrap'>{t('appLog.table.header.userRate')}</td>
<td className='whitespace-nowrap'>{t('appLog.table.header.adminRate')}</td>
<td className='whitespace-nowrap'>{t('appLog.table.header.updatedTime')}</td>
<td className='whitespace-nowrap'>{t('appLog.table.header.time')}</td>
<td className='pl-2 pr-1 w-5 rounded-l-lg bg-background-section-burn whitespace-nowrap'></td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{isChatMode ? t('appLog.table.header.summary') : t('appLog.table.header.input')}</td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.endUser')}</td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{isChatMode ? t('appLog.table.header.messageCount') : t('appLog.table.header.output')}</td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.userRate')}</td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.adminRate')}</td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.updatedTime')}</td>
<td className='pl-3 py-1.5 rounded-r-lg bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.time')}</td>
</tr> </tr>
</thead> </thead>
<tbody className="text-gray-500">
<tbody className="text-text-secondary system-sm-regular">
{logs.data.map((log: any) => { {logs.data.map((log: any) => {
const endUser = log.from_end_user_session_id || log.from_account_name const endUser = log.from_end_user_session_id || log.from_account_name
const leftValue = get(log, isChatMode ? 'name' : 'message.inputs.query') || (!isChatMode ? (get(log, 'message.query') || get(log, 'message.inputs.default_input')) : '') || '' const leftValue = get(log, isChatMode ? 'name' : 'message.inputs.query') || (!isChatMode ? (get(log, 'message.query') || get(log, 'message.inputs.default_input')) : '') || ''
const rightValue = get(log, isChatMode ? 'message_count' : 'message.answer') const rightValue = get(log, isChatMode ? 'message_count' : 'message.answer')
return <tr return <tr
key={log.id} key={log.id}
className={`border-b border-gray-200 h-8 hover:bg-gray-50 cursor-pointer ${currentConversation?.id !== log.id ? '' : 'bg-gray-50'}`}
className={cn('border-b border-divider-subtle hover:bg-background-default-hover cursor-pointer', currentConversation?.id !== log.id ? '' : 'bg-background-default-hover')}
onClick={() => { onClick={() => {
setShowDrawer(true) setShowDrawer(true)
setCurrentConversation(log) setCurrentConversation(log)
}}> }}>
<td className='text-center align-middle'>{!log.read_at && <span className='inline-block bg-[#3F83F8] h-1.5 w-1.5 rounded'></span>}</td>
<td style={{ maxWidth: isChatMode ? 300 : 200 }}>
<td className='h-4'>
{!log.read_at && (
<div className='p-3 pr-0.5 flex items-center'>
<span className='inline-block bg-util-colors-blue-blue-500 h-1.5 w-1.5 rounded'></span>
</div>
)}
</td>
<td className='p-3 pr-2 w-[160px]' style={{ maxWidth: isChatMode ? 300 : 200 }}>
{renderTdValue(leftValue || t('appLog.table.empty.noChat'), !leftValue, isChatMode && log.annotated)} {renderTdValue(leftValue || t('appLog.table.empty.noChat'), !leftValue, isChatMode && log.annotated)}
</td> </td>
<td>{renderTdValue(endUser || defaultValue, !endUser)}</td>
<td style={{ maxWidth: isChatMode ? 100 : 200 }}>
<td className='p-3 pr-2'>{renderTdValue(endUser || defaultValue, !endUser)}</td>
<td className='p-3 pr-2' style={{ maxWidth: isChatMode ? 100 : 200 }}>
{renderTdValue(rightValue === 0 ? 0 : (rightValue || t('appLog.table.empty.noOutput')), !rightValue, !isChatMode && !!log.annotation?.content, log.annotation)} {renderTdValue(rightValue === 0 ? 0 : (rightValue || t('appLog.table.empty.noOutput')), !rightValue, !isChatMode && !!log.annotation?.content, log.annotation)}
</td> </td>
<td>
<td className='p-3 pr-2'>
{(!log.user_feedback_stats.like && !log.user_feedback_stats.dislike) {(!log.user_feedback_stats.like && !log.user_feedback_stats.dislike)
? renderTdValue(defaultValue, true) ? renderTdValue(defaultValue, true)
: <> : <>
</> </>
} }
</td> </td>
<td>
<td className='p-3 pr-2'>
{(!log.admin_feedback_stats.like && !log.admin_feedback_stats.dislike) {(!log.admin_feedback_stats.like && !log.admin_feedback_stats.dislike)
? renderTdValue(defaultValue, true) ? renderTdValue(defaultValue, true)
: <> : <>
</> </>
} }
</td> </td>
<td className='w-[160px]'>{formatTime(log.updated_at, t('appLog.dateTimeFormat') as string)}</td>
<td className='w-[160px]'>{formatTime(log.created_at, t('appLog.dateTimeFormat') as string)}</td>
<td className='w-[160px] p-3 pr-2'>{formatTime(log.updated_at, t('appLog.dateTimeFormat') as string)}</td>
<td className='w-[160px] p-3 pr-2'>{formatTime(log.created_at, t('appLog.dateTimeFormat') as string)}</td>
</tr> </tr>
})} })}
</tbody> </tbody>

+ 0
- 6
web/app/components/app/log/style.module.css 파일 보기

.logTable td {
padding: 7px 8px;
box-sizing: border-box;
max-width: 200px;
}

.pagination li { .pagination li {
list-style: none; list-style: none;
} }

+ 19
- 9
web/app/components/app/overview/settings/index.tsx 파일 보기

import s from './style.module.css' import s from './style.module.css'
import Modal from '@/app/components/base/modal' import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import AppIcon from '@/app/components/base/app-icon' import AppIcon from '@/app/components/base/app-icon'
import Switch from '@/app/components/base/switch' import Switch from '@/app/components/base/switch'
import { SimpleSelect } from '@/app/components/base/select' import { SimpleSelect } from '@/app/components/base/select'
} }
} }


const onDesChange = (value: string) => {
setInputInfo(item => ({ ...item, desc: value }))
}

return ( return (
<> <>
<Modal <Modal
background={appIcon.type === 'image' ? undefined : appIcon.background} background={appIcon.type === 'image' ? undefined : appIcon.background}
imageUrl={appIcon.type === 'image' ? appIcon.url : undefined} imageUrl={appIcon.type === 'image' ? appIcon.url : undefined}
/> />
<input className={`flex-grow rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
<Input
className='grow h-10'
value={inputInfo.title} value={inputInfo.title}
onChange={onChange('title')} onChange={onChange('title')}
placeholder={t('app.appNamePlaceholder') || ''} placeholder={t('app.appNamePlaceholder') || ''}
</div> </div>
<div className={`mt-6 font-medium ${s.settingTitle} text-gray-900 `}>{t(`${prefixSettings}.webDesc`)}</div> <div className={`mt-6 font-medium ${s.settingTitle} text-gray-900 `}>{t(`${prefixSettings}.webDesc`)}</div>
<p className={`mt-1 ${s.settingsTip} text-gray-500`}>{t(`${prefixSettings}.webDescTip`)}</p> <p className={`mt-1 ${s.settingsTip} text-gray-500`}>{t(`${prefixSettings}.webDescTip`)}</p>
<textarea
rows={3}
className={`mt-2 pt-2 pb-2 px-3 rounded-lg bg-gray-100 w-full ${s.settingsTip} text-gray-900`}
<Textarea
className='mt-2'
value={inputInfo.desc} value={inputInfo.desc}
onChange={onChange('desc')}
onChange={e => onDesChange(e.target.value)}
placeholder={t(`${prefixSettings}.webDescPlaceholder`) as string} placeholder={t(`${prefixSettings}.webDescPlaceholder`) as string}
/> />
{isChatBot && ( {isChatBot && (


{isChat && <> <div className={`mt-8 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.chatColorTheme`)}</div> {isChat && <> <div className={`mt-8 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.chatColorTheme`)}</div>
<p className={`mt-1 ${s.settingsTip} text-gray-500`}>{t(`${prefixSettings}.chatColorThemeDesc`)}</p> <p className={`mt-1 ${s.settingsTip} text-gray-500`}>{t(`${prefixSettings}.chatColorThemeDesc`)}</p>
<input className={`w-full mt-2 rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
<Input
className='mt-2 h-10'
value={inputInfo.chatColorTheme ?? ''} value={inputInfo.chatColorTheme ?? ''}
onChange={onChange('chatColorTheme')} onChange={onChange('chatColorTheme')}
placeholder='E.g #A020F0' placeholder='E.g #A020F0'
{isShowMore && <> {isShowMore && <>
<hr className='w-full mt-6' /> <hr className='w-full mt-6' />
<div className={`mt-6 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.more.copyright`)}</div> <div className={`mt-6 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.more.copyright`)}</div>
<input className={`w-full mt-2 rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
<Input
className='mt-2 h-10'
value={inputInfo.copyright} value={inputInfo.copyright}
onChange={onChange('copyright')} onChange={onChange('copyright')}
placeholder={t(`${prefixSettings}.more.copyRightPlaceholder`) as string} placeholder={t(`${prefixSettings}.more.copyRightPlaceholder`) as string}
components={{ privacyPolicyLink: <Link href={'https://docs.dify.ai/user-agreement/privacy-policy'} target='_blank' rel='noopener noreferrer' className='text-primary-600' /> }} components={{ privacyPolicyLink: <Link href={'https://docs.dify.ai/user-agreement/privacy-policy'} target='_blank' rel='noopener noreferrer' className='text-primary-600' /> }}
/> />
</p> </p>
<input className={`w-full mt-2 rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
<Input
className='mt-2 h-10'
value={inputInfo.privacyPolicy} value={inputInfo.privacyPolicy}
onChange={onChange('privacyPolicy')} onChange={onChange('privacyPolicy')}
placeholder={t(`${prefixSettings}.more.privacyPolicyPlaceholder`) as string} placeholder={t(`${prefixSettings}.more.privacyPolicyPlaceholder`) as string}
/> />
<div className={`mt-8 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.more.customDisclaimer`)}</div> <div className={`mt-8 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.more.customDisclaimer`)}</div>
<p className={`mt-1 ${s.settingsTip} text-gray-500`}>{t(`${prefixSettings}.more.customDisclaimerTip`)}</p> <p className={`mt-1 ${s.settingsTip} text-gray-500`}>{t(`${prefixSettings}.more.customDisclaimerTip`)}</p>
<input className={`w-full mt-2 rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
<Input
className='mt-2 h-10'
value={inputInfo.customDisclaimer} value={inputInfo.customDisclaimer}
onChange={onChange('customDisclaimer')} onChange={onChange('customDisclaimer')}
placeholder={t(`${prefixSettings}.more.customDisclaimerPlaceholder`) as string} placeholder={t(`${prefixSettings}.more.customDisclaimerPlaceholder`) as string}

+ 0
- 5
web/app/components/app/overview/settings/style.module.css 파일 보기

.policy { .policy {
font-size: 0.75rem; font-size: 0.75rem;
line-height: 1.125rem; line-height: 1.125rem;
}

.projectName {
font-size: 0.875rem;
line-height: 2.5rem;
} }

+ 4
- 0
web/app/components/app/store.ts 파일 보기

showPromptLogModal: boolean showPromptLogModal: boolean
showAgentLogModal: boolean showAgentLogModal: boolean
showMessageLogModal: boolean showMessageLogModal: boolean
showAppConfigureFeaturesModal: boolean
} }


type Action = { type Action = {
setShowPromptLogModal: (showPromptLogModal: boolean) => void setShowPromptLogModal: (showPromptLogModal: boolean) => void
setShowAgentLogModal: (showAgentLogModal: boolean) => void setShowAgentLogModal: (showAgentLogModal: boolean) => void
setShowMessageLogModal: (showMessageLogModal: boolean) => void setShowMessageLogModal: (showMessageLogModal: boolean) => void
setShowAppConfigureFeaturesModal: (showAppConfigureFeaturesModal: boolean) => void
} }


export const useStore = create<State & Action>(set => ({ export const useStore = create<State & Action>(set => ({
} }
} }
}), }),
showAppConfigureFeaturesModal: false,
setShowAppConfigureFeaturesModal: showAppConfigureFeaturesModal => set(() => ({ showAppConfigureFeaturesModal })),
})) }))

+ 6
- 4
web/app/components/app/switch-app-modal/index.tsx 파일 보기

import AppIconPicker from '../../base/app-icon-picker' import AppIconPicker from '../../base/app-icon-picker'
import s from './style.module.css' import s from './style.module.css'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import Checkbox from '@/app/components/base/checkbox'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal' import Modal from '@/app/components/base/modal'
import Confirm from '@/app/components/base/confirm' import Confirm from '@/app/components/base/confirm'
import { ToastContext } from '@/app/components/base/toast' import { ToastContext } from '@/app/components/base/toast'
background={appIcon.type === 'image' ? undefined : appIcon.background} background={appIcon.type === 'image' ? undefined : appIcon.background}
imageUrl={appIcon.type === 'image' ? appIcon.url : undefined} imageUrl={appIcon.type === 'image' ? appIcon.url : undefined}
/> />
<input
<Input
value={name} value={name}
onChange={e => setName(e.target.value)} onChange={e => setName(e.target.value)}
placeholder={t('app.newApp.appNamePlaceholder') || ''} placeholder={t('app.newApp.appNamePlaceholder') || ''}
className='grow h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg border border-transparent outline-none appearance-none caret-primary-600 placeholder:text-gray-400 hover:bg-gray-50 hover:border hover:border-gray-300 focus:bg-gray-50 focus:border focus:border-gray-300 focus:shadow-xs'
className='grow h-10'
/> />
</div> </div>
{showAppIconPicker && <AppIconPicker {showAppIconPicker && <AppIconPicker
{isAppsFull && <AppsFull loc='app-switch' />} {isAppsFull && <AppsFull loc='app-switch' />}
<div className='pt-6 flex justify-between items-center'> <div className='pt-6 flex justify-between items-center'>
<div className='flex items-center'> <div className='flex items-center'>
<input id="removeOriginal" type="checkbox" checked={removeOriginal} onChange={() => setRemoveOriginal(!removeOriginal)} className="w-4 h-4 rounded border-gray-300 text-blue-700 cursor-pointer focus:ring-blue-700" />
<label htmlFor="removeOriginal" className="ml-2 text-sm leading-5 text-gray-700 cursor-pointer">{t('app.removeOriginal')}</label>
<Checkbox className='shrink-0' checked={removeOriginal} onCheck={() => setRemoveOriginal(!removeOriginal)} />
<div className="ml-2 text-sm leading-5 text-gray-700 cursor-pointer" onClick={() => setRemoveOriginal(!removeOriginal)}>{t('app.removeOriginal')}</div>
</div> </div>
<div className='flex items-center'> <div className='flex items-center'>
<Button className='mr-2' onClick={onClose}>{t('app.newApp.Cancel')}</Button> <Button className='mr-2' onClick={onClose}>{t('app.newApp.Cancel')}</Button>

+ 2
- 2
web/app/components/app/text-generate/item/index.tsx 파일 보기

import { Bookmark } from '@/app/components/base/icons/src/vender/line/general' import { Bookmark } from '@/app/components/base/icons/src/vender/line/general'
import { Stars02 } from '@/app/components/base/icons/src/vender/line/weather' import { Stars02 } from '@/app/components/base/icons/src/vender/line/weather'
import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows' import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows'
import AnnotationCtrlBtn from '@/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-btn'
import { fetchTextGenerationMessage } from '@/service/debug' import { fetchTextGenerationMessage } from '@/service/debug'
import AnnotationCtrlBtn from '@/app/components/app/configuration/toolbox/annotation/annotation-ctrl-btn'
import EditReplyModal from '@/app/components/app/annotation/edit-annotation-modal' import EditReplyModal from '@/app/components/app/annotation/edit-annotation-modal'
import { useStore as useAppStore } from '@/app/components/app/store' import { useStore as useAppStore } from '@/app/components/app/store'
import WorkflowProcessItem from '@/app/components/base/chat/chat/answer/workflow-process' import WorkflowProcessItem from '@/app/components/base/chat/chat/answer/workflow-process'
<div className={`flex ${contentClassName}`}> <div className={`flex ${contentClassName}`}>
<div className='grow w-0'> <div className='grow w-0'>
{siteInfo && siteInfo.show_workflow_steps && workflowProcessData && ( {siteInfo && siteInfo.show_workflow_steps && workflowProcessData && (
<WorkflowProcessItem grayBg hideInfo data={workflowProcessData} expand={workflowProcessData.expand} hideProcessDetail={hideProcessDetail} />
<WorkflowProcessItem data={workflowProcessData} expand={workflowProcessData.expand} hideProcessDetail={hideProcessDetail} />
)} )}
{workflowProcessData && !isError && ( {workflowProcessData && !isError && (
<ResultTab data={workflowProcessData} content={content} currentTab={currentTab} onCurrentTabChange={setCurrentTab} /> <ResultTab data={workflowProcessData} content={content} currentTab={currentTab} onCurrentTabChange={setCurrentTab} />

+ 21
- 10
web/app/components/app/text-generate/item/result-tab.tsx 파일 보기

} from 'react' } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
// import Loading from '@/app/components/base/loading'
import { Markdown } from '@/app/components/base/markdown' import { Markdown } from '@/app/components/base/markdown'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import type { WorkflowProcess } from '@/app/components/base/chat/types' import type { WorkflowProcess } from '@/app/components/base/chat/types'
// import { WorkflowRunningStatus } from '@/app/components/workflow/types'
import { FileList } from '@/app/components/base/file-uploader'


const ResultTab = ({ const ResultTab = ({
data, data,
)} )}
<div className={cn('grow bg-white')}> <div className={cn('grow bg-white')}>
{currentTab === 'RESULT' && ( {currentTab === 'RESULT' && (
<Markdown content={data?.resultText || ''} />
<>
<Markdown content={data?.resultText || ''} />
{!!data?.files?.length && (
<FileList
files={data?.files}
showDeleteAction={false}
showDownloadAction
canPreview
/>
)}
</>
)} )}
{currentTab === 'DETAIL' && content && ( {currentTab === 'DETAIL' && content && (
<CodeEditor
readOnly
title={<div>JSON OUTPUT</div>}
language={CodeLanguage.json}
value={content}
isJSONStringifyBeauty
/>
<div className='mt-1'>
<CodeEditor
readOnly
title={<div>JSON OUTPUT</div>}
language={CodeLanguage.json}
value={content}
isJSONStringifyBeauty
/>
</div>
)} )}
</div> </div>
</div> </div>

+ 3
- 3
web/app/components/app/workflow-log/detail.tsx 파일 보기

const { t } = useTranslation() const { t } = useTranslation()


return ( return (
<div className='grow relative flex flex-col py-3'>
<div className='grow relative flex flex-col pt-3'>
<span className='absolute right-3 top-4 p-1 cursor-pointer z-20' onClick={onClose}> <span className='absolute right-3 top-4 p-1 cursor-pointer z-20' onClick={onClose}>
<RiCloseLine className='w-4 h-4 text-gray-500' />
<RiCloseLine className='w-4 h-4 text-text-tertiary' />
</span> </span>
<h1 className='shrink-0 px-4 py-1 text-md font-semibold text-gray-900'>{t('appLog.runDetail.workflowTitle')}</h1>
<h1 className='shrink-0 px-4 py-1 text-text-primary system-xl-semibold'>{t('appLog.runDetail.workflowTitle')}</h1>
<Run runID={runID}/> <Run runID={runID}/>
</div> </div>
) )

+ 26
- 38
web/app/components/app/workflow-log/filter.tsx 파일 보기

import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import {
MagnifyingGlassIcon,
} from '@heroicons/react/24/solid'
import type { QueryParam } from './index' import type { QueryParam } from './index'
import { SimpleSelect } from '@/app/components/base/select'
import Chip from '@/app/components/base/chip'
import Input from '@/app/components/base/input'


type IFilterProps = { type IFilterProps = {
queryParams: QueryParam queryParams: QueryParam
const Filter: FC<IFilterProps> = ({ queryParams, setQueryParams }: IFilterProps) => { const Filter: FC<IFilterProps> = ({ queryParams, setQueryParams }: IFilterProps) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div className='flex flex-row flex-wrap gap-y-2 gap-x-4 items-center mb-4 text-gray-900 text-base'>
<div className="relative rounded-md">
<SimpleSelect
defaultValue={'all'}
className='!min-w-[100px]'
onSelect={
(item) => {
if (!item.value)
return
setQueryParams({ ...queryParams, status: item.value as string })
}
}
items={[{ value: 'all', name: 'All' },
{ value: 'succeeded', name: 'Success' },
{ value: 'failed', name: 'Fail' },
{ value: 'stopped', name: 'Stop' },
]}
/>
</div>
<div className="relative">
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<MagnifyingGlassIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div>
<input
type="text"
name="query"
className="block w-[240px] bg-gray-100 shadow-sm rounded-md border-0 py-1.5 pl-10 text-gray-900 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-gray-200 focus-visible:outline-none sm:text-sm sm:leading-6"
placeholder={t('common.operation.search')!}
value={queryParams.keyword}
onChange={(e) => {
setQueryParams({ ...queryParams, keyword: e.target.value })
}}
/>
</div>
<div className='flex flex-row flex-wrap gap-2 mb-2'>
<Chip
value={queryParams.status || 'all'}
onSelect={(item) => {
setQueryParams({ ...queryParams, status: item.value as string })
}}
onClear={() => setQueryParams({ ...queryParams, status: 'all' })}
items={[{ value: 'all', name: 'All' },
{ value: 'succeeded', name: 'Success' },
{ value: 'failed', name: 'Fail' },
{ value: 'stopped', name: 'Stop' },
]}
/>
<Input
wrapperClassName='w-[200px]'
showLeftIcon
showClearIcon
value={queryParams.keyword}
placeholder={t('common.operation.search')!}
onChange={(e) => {
setQueryParams({ ...queryParams, keyword: e.target.value })
}}
onClear={() => setQueryParams({ ...queryParams, keyword: '' })}
/>
</div> </div>
) )
} }

+ 6
- 6
web/app/components/app/workflow-log/index.tsx 파일 보기

const pathSegments = pathname.split('/') const pathSegments = pathname.split('/')
pathSegments.pop() pathSegments.pop()
return <div className='flex items-center justify-center h-full'> return <div className='flex items-center justify-center h-full'>
<div className='bg-gray-50 w-[560px] h-fit box-border px-5 py-4 rounded-2xl'>
<span className='text-gray-700 font-semibold'>{t('appLog.table.empty.element.title')}<ThreeDotsIcon className='inline relative -top-3 -left-1.5' /></span>
<div className='mt-2 text-gray-500 text-sm font-normal'>
<div className='bg-background-section-burn w-[560px] h-fit box-border px-5 py-4 rounded-2xl'>
<span className='text-text-secondary system-md-semibold'>{t('appLog.table.empty.element.title')}<ThreeDotsIcon className='inline relative -top-3 -left-1.5' /></span>
<div className='mt-2 text-text-tertiary system-sm-regular'>
<Trans <Trans
i18nKey="appLog.table.empty.element.content" i18nKey="appLog.table.empty.element.content"
components={{ shareLink: <Link href={`${pathSegments.join('/')}/overview`} className='text-primary-600' />, testLink: <Link href={appUrl} className='text-primary-600' target='_blank' rel='noopener noreferrer' /> }}
components={{ shareLink: <Link href={`${pathSegments.join('/')}/overview`} className='text-util-colors-blue-blue-600' />, testLink: <Link href={appUrl} className='text-util-colors-blue-blue-600' target='_blank' rel='noopener noreferrer' /> }}
/> />
</div> </div>
</div> </div>


return ( return (
<div className='flex flex-col h-full'> <div className='flex flex-col h-full'>
<h1 className='text-md font-semibold text-gray-900'>{t('appLog.workflowTitle')}</h1>
<p className='flex text-sm font-normal text-gray-500'>{t('appLog.workflowSubtitle')}</p>
<h1 className='text-text-primary system-xl-semibold'>{t('appLog.workflowTitle')}</h1>
<p className='text-text-tertiary system-sm-regular'>{t('appLog.workflowSubtitle')}</p>
<div className='flex flex-col py-4 flex-1'> <div className='flex flex-col py-4 flex-1'>
<Filter queryParams={queryParams} setQueryParams={setQueryParams} /> <Filter queryParams={queryParams} setQueryParams={setQueryParams} />
{/* workflow log */} {/* workflow log */}

+ 34
- 30
web/app/components/app/workflow-log/list.tsx 파일 보기

import type { FC } from 'react' import type { FC } from 'react'
import React, { useState } from 'react' import React, { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import s from './style.module.css'
// import s from './style.module.css'
import DetailPanel from './detail' import DetailPanel from './detail'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import type { WorkflowAppLogDetail, WorkflowLogsResponse } from '@/models/log' import type { WorkflowAppLogDetail, WorkflowLogsResponse } from '@/models/log'
const statusTdRender = (status: string) => { const statusTdRender = (status: string) => {
if (status === 'succeeded') { if (status === 'succeeded') {
return ( return (
<div className='inline-flex items-center gap-1'>
<div className='inline-flex items-center gap-1 system-xs-semibold-uppercase'>
<Indicator color={'green'} /> <Indicator color={'green'} />
<span>Success</span>
<span className='text-util-colors-green-green-600'>Success</span>
</div> </div>
) )
} }
if (status === 'failed') { if (status === 'failed') {
return ( return (
<div className='inline-flex items-center gap-1'>
<div className='inline-flex items-center gap-1 system-xs-semibold-uppercase'>
<Indicator color={'red'} /> <Indicator color={'red'} />
<span className='text-red-600'>Fail</span>
<span className='text-util-colors-red-red-600'>Fail</span>
</div> </div>
) )
} }
if (status === 'stopped') { if (status === 'stopped') {
return ( return (
<div className='inline-flex items-center gap-1'>
<div className='inline-flex items-center gap-1 system-xs-semibold-uppercase'>
<Indicator color={'yellow'} /> <Indicator color={'yellow'} />
<span>Stop</span>
<span className='text-util-colors-warning-warning-600'>Stop</span>
</div> </div>
) )
} }
if (status === 'running') { if (status === 'running') {
return ( return (
<div className='inline-flex items-center gap-1'>
<div className='inline-flex items-center gap-1 system-xs-semibold-uppercase'>
<Indicator color={'blue'} /> <Indicator color={'blue'} />
<span className='text-primary-600'>Running</span>
<span className='text-util-colors-blue-light-blue-light-600'>Running</span>
</div> </div>
) )
} }


return ( return (
<div className='overflow-x-auto'> <div className='overflow-x-auto'>
<table className={`w-full min-w-[440px] border-collapse border-0 text-sm mt-3 ${s.logTable}`}>
<thead className="h-8 !pl-3 py-2 leading-[18px] border-b border-gray-200 text-xs text-gray-500 font-medium">
<table className={cn('mt-2 w-full min-w-[440px] border-collapse border-0')}>
<thead className='system-xs-medium-uppercase text-text-tertiary'>
<tr> <tr>
<td className='w-[1.375rem] whitespace-nowrap'></td>
<td className='whitespace-nowrap'>{t('appLog.table.header.startTime')}</td>
<td className='whitespace-nowrap'>{t('appLog.table.header.status')}</td>
<td className='whitespace-nowrap'>{t('appLog.table.header.runtime')}</td>
<td className='whitespace-nowrap'>{t('appLog.table.header.tokens')}</td>
<td className='whitespace-nowrap'>{t('appLog.table.header.user')}</td>
{/* <td className='whitespace-nowrap'>{t('appLog.table.header.version')}</td> */}
<td className='pl-2 pr-1 w-5 rounded-l-lg bg-background-section-burn whitespace-nowrap'></td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.startTime')}</td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.status')}</td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.runtime')}</td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.tokens')}</td>
<td className='pl-3 py-1.5 rounded-r-lg bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.user')}</td>
</tr> </tr>
</thead> </thead>
<tbody className="text-gray-700 text-[13px]">
<tbody className="text-text-secondary system-sm-regular">
{logs.data.map((log: WorkflowAppLogDetail) => { {logs.data.map((log: WorkflowAppLogDetail) => {
const endUser = log.created_by_end_user ? log.created_by_end_user.session_id : log.created_by_account ? log.created_by_account.name : defaultValue const endUser = log.created_by_end_user ? log.created_by_end_user.session_id : log.created_by_account ? log.created_by_account.name : defaultValue
return <tr return <tr
key={log.id} key={log.id}
className={`border-b border-gray-200 h-8 hover:bg-gray-50 cursor-pointer ${currentLog?.id !== log.id ? '' : 'bg-gray-50'}`}
className={cn('border-b border-divider-subtle hover:bg-background-default-hover cursor-pointer', currentLog?.id !== log.id ? '' : 'bg-background-default-hover')}
onClick={() => { onClick={() => {
setCurrentLog(log) setCurrentLog(log)
setShowDrawer(true) setShowDrawer(true)
}}> }}>
<td className='text-center align-middle'>{!log.read_at && <span className='inline-block bg-[#3F83F8] h-1.5 w-1.5 rounded'></span>}</td>
<td className='w-[160px]'>{formatTime(log.created_at, t('appLog.dateTimeFormat') as string)}</td>
<td>{statusTdRender(log.workflow_run.status)}</td>
<td>
<td className='h-4'>
{!log.read_at && (
<div className='p-3 pr-0.5 flex items-center'>
<span className='inline-block bg-util-colors-blue-blue-500 h-1.5 w-1.5 rounded'></span>
</div>
)}
</td>
<td className='p-3 pr-2 w-[160px]'>{formatTime(log.created_at, t('appLog.dateTimeFormat') as string)}</td>
<td className='p-3 pr-2'>{statusTdRender(log.workflow_run.status)}</td>
<td className='p-3 pr-2'>
<div className={cn( <div className={cn(
log.workflow_run.elapsed_time === 0 && 'text-gray-400',
log.workflow_run.elapsed_time === 0 && 'text-text-quaternary',
)}>{`${log.workflow_run.elapsed_time.toFixed(3)}s`}</div> )}>{`${log.workflow_run.elapsed_time.toFixed(3)}s`}</div>
</td> </td>
<td>{log.workflow_run.total_tokens}</td>
<td>
<div className={cn(endUser === defaultValue ? 'text-gray-400' : 'text-gray-700', 'text-sm overflow-hidden text-ellipsis whitespace-nowrap')}>
<td className='p-3 pr-2'>{log.workflow_run.total_tokens}</td>
<td className='p-3 pr-2'>
<div className={cn(endUser === defaultValue ? 'text-text-quaternary' : 'text-text-secondary', 'overflow-hidden text-ellipsis whitespace-nowrap')}>
{endUser} {endUser}
</div> </div>
</td> </td>
{/* <td>VERSION</td> */}
</tr> </tr>
})} })}
</tbody> </tbody>
onClose={onCloseDrawer} onClose={onCloseDrawer}
mask={isMobile} mask={isMobile}
footer={null} footer={null}
panelClassname='mt-16 mx-2 sm:mr-2 mb-3 !p-0 !max-w-[600px] rounded-xl border border-gray-200'
panelClassname='mt-16 mx-2 sm:mr-2 mb-3 !p-0 !max-w-[600px] rounded-xl border border-components-panel-border'
> >
<DetailPanel onClose={onCloseDrawer} runID={currentLog?.workflow_run.id || ''} /> <DetailPanel onClose={onCloseDrawer} runID={currentLog?.workflow_run.id || ''} />
</Drawer> </Drawer>

+ 0
- 6
web/app/components/app/workflow-log/style.module.css 파일 보기

.logTable td {
padding: 7px 8px;
box-sizing: border-box;
max-width: 200px;
}

.pagination li { .pagination li {
list-style: none; list-style: none;
} }

+ 2
- 2
web/app/components/base/button/add-button.tsx 파일 보기

onClick, onClick,
}) => { }) => {
return ( return (
<div className={cn(className, 'p-1 rounded-md cursor-pointer hover:bg-gray-200 select-none')} onClick={onClick}>
<RiAddLine className='w-4 h-4 text-gray-500' />
<div className={cn(className, 'p-1 rounded-md cursor-pointer hover:bg-state-base-hover select-none')} onClick={onClick}>
<RiAddLine className='w-4 h-4 text-text-tertiary' />
</div> </div>
) )
} }

+ 27
- 33
web/app/components/base/chat/chat-with-history/chat-wrapper.tsx 파일 보기

getUrl, getUrl,
stopChatMessageResponding, stopChatMessageResponding,
} from '@/service/share' } from '@/service/share'
import AnswerIcon from '@/app/components/base/answer-icon'


const ChatWrapper = () => { const ChatWrapper = () => {
const { const {
appConfig, appConfig,
{ {
inputs: (currentConversationId ? currentConversationItem?.inputs : newConversationInputs) as any, inputs: (currentConversationId ? currentConversationItem?.inputs : newConversationInputs) as any,
promptVariables: inputsForms,
inputsForm: inputsForms,
}, },
appPrevChatList, appPrevChatList,
taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId), taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
useEffect(() => { useEffect(() => {
if (currentChatInstanceRef.current) if (currentChatInstanceRef.current)
currentChatInstanceRef.current.handleStop = handleStop currentChatInstanceRef.current.handleStop = handleStop
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])


const doSend: OnSend = useCallback((message, files, last_answer) => { const doSend: OnSend = useCallback((message, files, last_answer) => {
const data: any = { const data: any = {
query: message, query: message,
files,
inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs, inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs,
conversation_id: currentConversationId, conversation_id: currentConversationId,
parent_message_id: last_answer?.id || getLastAnswer(chatListRef.current)?.id || null, parent_message_id: last_answer?.id || getLastAnswer(chatListRef.current)?.id || null,
} }


if (appConfig?.file_upload?.image.enabled && files?.length)
data.files = files

handleSend( handleSend(
getUrl('chat-messages', isInstalledApp, appId || ''), getUrl('chat-messages', isInstalledApp, appId || ''),
data, data,
isMobile, isMobile,
]) ])


const answerIcon = (appData?.site && appData.site.use_icon_as_answer_icon)
? <AnswerIcon
iconType={appData.site.icon_type}
icon={appData.site.icon}
background={appData.site.icon_background}
imageUrl={appData.site.icon_url}
/>
: null

return ( return (
<Chat
appData={appData}
config={appConfig}
chatList={chatList}
isResponding={isResponding}
chatContainerInnerClassName={`mx-auto pt-6 w-full max-w-full ${isMobile && 'px-4'}`}
chatFooterClassName='pb-4'
chatFooterInnerClassName={`mx-auto w-full max-w-full ${isMobile && 'px-4'}`}
onSend={doSend}
onRegenerate={doRegenerate}
onStopResponding={handleStop}
chatNode={chatNode}
allToolIcons={appMeta?.tool_icons || {}}
onFeedback={handleFeedback}
suggestedQuestions={suggestedQuestions}
answerIcon={answerIcon}
hideProcessDetail
themeBuilder={themeBuilder}
/>
<div
className='h-full bg-chatbot-bg overflow-hidden'
>
<Chat
appData={appData}
config={appConfig}
chatList={chatList}
isResponding={isResponding}
chatContainerInnerClassName={`mx-auto pt-6 w-full max-w-[720px] ${isMobile && 'px-4'}`}
chatFooterClassName='pb-4'
chatFooterInnerClassName={`mx-auto w-full max-w-[720px] ${isMobile && 'px-4'}`}
onSend={doSend}
inputs={currentConversationId ? currentConversationItem?.inputs as any : newConversationInputs}
inputsForm={inputsForms}
onRegenerate={doRegenerate}
onStopResponding={handleStop}
chatNode={chatNode}
allToolIcons={appMeta?.tool_icons || {}}
onFeedback={handleFeedback}
suggestedQuestions={suggestedQuestions}
hideProcessDetail
themeBuilder={themeBuilder}
/>
</div>
) )
} }



+ 3
- 2
web/app/components/base/chat/chat-with-history/config-panel/form-input.tsx 파일 보기

import type { FC } from 'react' import type { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { memo } from 'react' import { memo } from 'react'
import Textarea from '@/app/components/base/textarea'


type InputProps = { type InputProps = {
form: any form: any


if (type === 'paragraph') { if (type === 'paragraph') {
return ( return (
<textarea
<Textarea
value={value} value={value}
className='grow h-[104px] rounded-lg bg-gray-100 px-2.5 py-2 outline-none appearance-none resize-none'
className='resize-none'
onChange={e => onChange(variable, e.target.value)} onChange={e => onChange(variable, e.target.value)}
placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`} placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
/> />

+ 34
- 4
web/app/components/base/chat/chat-with-history/config-panel/form.tsx 파일 보기

import { useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useChatWithHistoryContext } from '../context' import { useChatWithHistoryContext } from '../context'
import Input from './form-input' import Input from './form-input'
import { PortalSelect } from '@/app/components/base/select' import { PortalSelect } from '@/app/components/base/select'
import { InputVarType } from '@/app/components/workflow/types'
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'


const Form = () => { const Form = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { const {
inputsForms, inputsForms,
newConversationInputs, newConversationInputs,
newConversationInputsRef,
handleNewConversationInputsChange, handleNewConversationInputsChange,
isMobile, isMobile,
} = useChatWithHistoryContext() } = useChatWithHistoryContext()


const handleFormChange = useCallback((variable: string, value: string) => {
const handleFormChange = (variable: string, value: any) => {
handleNewConversationInputsChange({ handleNewConversationInputsChange({
...newConversationInputs,
...newConversationInputsRef.current,
[variable]: value, [variable]: value,
}) })
}, [newConversationInputs, handleNewConversationInputsChange])
}


const renderField = (form: any) => { const renderField = (form: any) => {
const { const {
/> />
) )
} }
if (form.type === InputVarType.singleFile) {
return (
<FileUploaderInAttachmentWrapper
value={newConversationInputs[variable] ? [newConversationInputs[variable]] : []}
onChange={files => handleFormChange(variable, files[0])}
fileConfig={{
allowed_file_types: form.allowed_file_types,
allowed_file_extensions: form.allowed_file_extensions,
allowed_file_upload_methods: form.allowed_file_upload_methods,
number_limits: 1,
}}
/>
)
}
if (form.type === InputVarType.multiFiles) {
return (
<FileUploaderInAttachmentWrapper
value={newConversationInputs[variable]}
onChange={files => handleFormChange(variable, files)}
fileConfig={{
allowed_file_types: form.allowed_file_types,
allowed_file_extensions: form.allowed_file_extensions,
allowed_file_upload_methods: form.allowed_file_upload_methods,
number_limits: form.max_length,
}}
/>
)
}


return ( return (
<PortalSelect <PortalSelect

+ 2
- 0
web/app/components/base/chat/chat-with-history/context.tsx 파일 보기

conversationList: AppConversationData['data'] conversationList: AppConversationData['data']
showConfigPanelBeforeChat: boolean showConfigPanelBeforeChat: boolean
newConversationInputs: Record<string, any> newConversationInputs: Record<string, any>
newConversationInputsRef: RefObject<Record<string, any>>
handleNewConversationInputsChange: (v: Record<string, any>) => void handleNewConversationInputsChange: (v: Record<string, any>) => void
inputsForms: any[] inputsForms: any[]
handleNewConversation: () => void handleNewConversation: () => void
conversationList: [], conversationList: [],
showConfigPanelBeforeChat: false, showConfigPanelBeforeChat: false,
newConversationInputs: {}, newConversationInputs: {},
newConversationInputsRef: { current: {} },
handleNewConversationInputsChange: () => {}, handleNewConversationInputsChange: () => {},
inputsForms: [], inputsForms: [],
handleNewConversation: () => {}, handleNewConversation: () => {},

+ 48
- 14
web/app/components/base/chat/chat-with-history/hooks.tsx 파일 보기

import { useToastContext } from '@/app/components/base/toast' import { useToastContext } from '@/app/components/base/toast'
import { changeLanguage } from '@/i18n/i18next-config' import { changeLanguage } from '@/i18n/i18next-config'
import { useAppFavicon } from '@/hooks/use-app-favicon' import { useAppFavicon } from '@/hooks/use-app-favicon'
import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app'


export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo]) const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo])
setNewConversationInputs(newInputs) setNewConversationInputs(newInputs)
}, []) }, [])
const inputsForms = useMemo(() => { const inputsForms = useMemo(() => {
return (appParams?.user_input_form || []).filter((item: any) => item.paragraph || item.select || item['text-input'] || item.number).map((item: any) => {
return (appParams?.user_input_form || []).filter((item: any) => !item.external_data_tool).map((item: any) => {
if (item.paragraph) { if (item.paragraph) {
return { return {
...item.paragraph, ...item.paragraph,
} }
} }


if (item['file-list']) {
return {
...item['file-list'],
type: 'file-list',
}
}

if (item.file) {
return {
...item.file,
type: 'file',
}
}

return { return {
...item['text-input'], ...item['text-input'],
type: 'text-input', type: 'text-input',


const { notify } = useToastContext() const { notify } = useToastContext()
const checkInputsRequired = useCallback((silent?: boolean) => { const checkInputsRequired = useCallback((silent?: boolean) => {
if (inputsForms.length) {
for (let i = 0; i < inputsForms.length; i += 1) {
const item = inputsForms[i]

if (item.required && !newConversationInputsRef.current[item.variable]) {
if (!silent) {
notify({
type: 'error',
message: t('appDebug.errorMessage.valueOfVarRequired', { key: item.variable }),
})
}
let hasEmptyInput = ''
let fileIsUploading = false
const requiredVars = inputsForms.filter(({ required }) => required)
if (requiredVars.length) {
requiredVars.forEach(({ variable, label, type }) => {
if (hasEmptyInput)
return

if (fileIsUploading)
return return

if (!newConversationInputsRef.current[variable] && !silent)
hasEmptyInput = label as string

if ((type === InputVarType.singleFile || type === InputVarType.multiFiles) && newConversationInputsRef.current[variable] && !silent) {
const files = newConversationInputsRef.current[variable]
if (Array.isArray(files))
fileIsUploading = files.find(item => item.transferMethod === TransferMethod.local_file && !item.uploadedId)
else
fileIsUploading = files.transferMethod === TransferMethod.local_file && !files.uploadedId
} }
}
return true
})
}

if (hasEmptyInput) {
notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput }) })
return false
}

if (fileIsUploading) {
notify({ type: 'info', message: t('appDebug.errorMessage.waitForFileUpload') })
return
} }


return true return true
setShowConfigPanelBeforeChat, setShowConfigPanelBeforeChat,
setShowNewConversationItemInList, setShowNewConversationItemInList,
newConversationInputs, newConversationInputs,
newConversationInputsRef,
handleNewConversationInputsChange, handleNewConversationInputsChange,
inputsForms, inputsForms,
handleNewConversation, handleNewConversation,

+ 2
- 0
web/app/components/base/chat/chat-with-history/index.tsx 파일 보기

conversationList, conversationList,
showConfigPanelBeforeChat, showConfigPanelBeforeChat,
newConversationInputs, newConversationInputs,
newConversationInputsRef,
handleNewConversationInputsChange, handleNewConversationInputsChange,
inputsForms, inputsForms,
handleNewConversation, handleNewConversation,
conversationList, conversationList,
showConfigPanelBeforeChat, showConfigPanelBeforeChat,
newConversationInputs, newConversationInputs,
newConversationInputsRef,
handleNewConversationInputsChange, handleNewConversationInputsChange,
inputsForms, inputsForms,
handleNewConversation, handleNewConversation,

+ 13
- 16
web/app/components/base/chat/chat/answer/agent-content.tsx 파일 보기

import { memo } from 'react' import { memo } from 'react'
import type { import type {
ChatItem, ChatItem,
VisionFile,
} from '../../types' } from '../../types'
import { Markdown } from '@/app/components/base/markdown' import { Markdown } from '@/app/components/base/markdown'
import Thought from '@/app/components/base/chat/chat/thought' import Thought from '@/app/components/base/chat/chat/thought'
import ImageGallery from '@/app/components/base/image-gallery'
import type { Emoji } from '@/app/components/tools/types'
import { FileList } from '@/app/components/base/file-uploader'
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'


type AgentContentProps = { type AgentContentProps = {
item: ChatItem item: ChatItem
responding?: boolean responding?: boolean
allToolIcons?: Record<string, string | Emoji>
} }
const AgentContent: FC<AgentContentProps> = ({ const AgentContent: FC<AgentContentProps> = ({
item, item,
responding, responding,
allToolIcons,
}) => { }) => {
const { const {
annotation, annotation,
agent_thoughts, agent_thoughts,
} = item } = item


const getImgs = (list?: VisionFile[]) => {
if (!list)
return []
return list.filter(file => file.type === 'image' && file.belongs_to === 'assistant')
}

if (annotation?.logAnnotation) if (annotation?.logAnnotation)
return <Markdown content={annotation?.logAnnotation.content || ''} /> return <Markdown content={annotation?.logAnnotation.content || ''} />


return ( return (
<div> <div>
{agent_thoughts?.map((thought, index) => ( {agent_thoughts?.map((thought, index) => (
<div key={index}>
<div key={index} className='px-2 py-1'>
{thought.thought && ( {thought.thought && (
<Markdown content={thought.thought} /> <Markdown content={thought.thought} />
)} )}
{!!thought.tool && ( {!!thought.tool && (
<Thought <Thought
thought={thought} thought={thought}
allToolIcons={allToolIcons || {}}
isFinished={!!thought.observation || !responding} isFinished={!!thought.observation || !responding}
/> />
)} )}


{getImgs(thought.message_files).length > 0 && (
<ImageGallery srcs={getImgs(thought.message_files).map(file => file.url)} />
)}
{
!!thought.message_files?.length && (
<FileList
files={getProcessedFilesFromResponse(thought.message_files.map((item: any) => ({ ...item, related_id: item.id })))}
showDeleteAction={false}
showDownloadAction={true}
canPreview={true}
/>
)
}
</div> </div>
))} ))}
</div> </div>

+ 11
- 2
web/app/components/base/chat/chat/answer/basic-content.tsx 파일 보기

import { memo } from 'react' import { memo } from 'react'
import type { ChatItem } from '../../types' import type { ChatItem } from '../../types'
import { Markdown } from '@/app/components/base/markdown' import { Markdown } from '@/app/components/base/markdown'
import cn from '@/utils/classnames'


type BasicContentProps = { type BasicContentProps = {
item: ChatItem item: ChatItem
} = item } = item


if (annotation?.logAnnotation) if (annotation?.logAnnotation)
return <Markdown content={annotation?.logAnnotation.content || ''} />
return <Markdown content={annotation?.logAnnotation.content || ''} className='px-2 py-1' />


return <Markdown content={content} className={`${item.isError && '!text-[#F04438]'}`} />
return (
<Markdown
className={cn(
'px-2 py-1',
item.isError && '!text-[#F04438]',
)}
content={content}
/>
)
} }


export default memo(BasicContent) export default memo(BasicContent)

+ 25
- 6
web/app/components/base/chat/chat/answer/index.tsx 파일 보기

import LoadingAnim from '@/app/components/base/chat/chat/loading-anim' import LoadingAnim from '@/app/components/base/chat/chat/loading-anim'
import Citation from '@/app/components/base/chat/chat/citation' import Citation from '@/app/components/base/chat/chat/citation'
import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal/edit-item' import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal/edit-item'
import type { Emoji } from '@/app/components/tools/types'
import type { AppData } from '@/models/share' import type { AppData } from '@/models/share'
import AnswerIcon from '@/app/components/base/answer-icon' import AnswerIcon from '@/app/components/base/answer-icon'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { FileList } from '@/app/components/base/file-uploader'


type AnswerProps = { type AnswerProps = {
item: ChatItem item: ChatItem
config?: ChatConfig config?: ChatConfig
answerIcon?: ReactNode answerIcon?: ReactNode
responding?: boolean responding?: boolean
allToolIcons?: Record<string, string | Emoji>
showPromptLog?: boolean showPromptLog?: boolean
chatAnswerContainerInner?: string chatAnswerContainerInner?: string
hideProcessDetail?: boolean hideProcessDetail?: boolean
config, config,
answerIcon, answerIcon,
responding, responding,
allToolIcons,
showPromptLog, showPromptLog,
chatAnswerContainerInner, chatAnswerContainerInner,
hideProcessDetail, hideProcessDetail,
more, more,
annotation, annotation,
workflowProcess, workflowProcess,
allFiles,
message_files,
} = item } = item
const hasAgentThoughts = !!agent_thoughts?.length const hasAgentThoughts = !!agent_thoughts?.length


<WorkflowProcess <WorkflowProcess
data={workflowProcess} data={workflowProcess}
item={item} item={item}
hideInfo
hideProcessDetail={hideProcessDetail} hideProcessDetail={hideProcessDetail}
/> />
) )
<WorkflowProcess <WorkflowProcess
data={workflowProcess} data={workflowProcess}
item={item} item={item}
hideInfo
hideProcessDetail={hideProcessDetail} hideProcessDetail={hideProcessDetail}
/> />
) )
<AgentContent <AgentContent
item={item} item={item}
responding={responding} responding={responding}
allToolIcons={allToolIcons}
/>
)
}
{
!!allFiles?.length && (
<FileList
className='my-1'
files={allFiles}
showDeleteAction={false}
showDownloadAction
canPreview
/>
)
}
{
!!message_files?.length && (
<FileList
className='my-1'
files={message_files}
showDeleteAction={false}
showDownloadAction
canPreview
/> />
) )
} }

+ 1
- 1
web/app/components/base/chat/chat/answer/operation.tsx 파일 보기

import CopyBtn from '@/app/components/base/copy-btn' import CopyBtn from '@/app/components/base/copy-btn'
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication' import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
import AudioBtn from '@/app/components/base/audio-btn' import AudioBtn from '@/app/components/base/audio-btn'
import AnnotationCtrlBtn from '@/app/components/app/configuration/toolbox/annotation/annotation-ctrl-btn'
import AnnotationCtrlBtn from '@/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-btn'
import EditReplyModal from '@/app/components/app/annotation/edit-annotation-modal' import EditReplyModal from '@/app/components/app/annotation/edit-annotation-modal'
import { import {
ThumbsDown, ThumbsDown,

+ 0
- 0
web/app/components/base/chat/chat/answer/tool-detail.tsx 파일 보기


이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.

Loading…
취소
저장