From b683b90b4e50dd3f8ef965fee7565a4555418f33 Mon Sep 17 00:00:00 2001 From: longbingljw Date: Tue, 11 Nov 2025 10:47:47 +0800 Subject: [PATCH] fix:update latest commits and solve confilcts (#54) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: adding some web tests (#27792) * feat: add validation to prevent saving empty opening statement in conversation opener modal (#27843) * fix(web): improve the consistency of the inputs-form UI (#27837) * fix(web): increase z-index of PortalToFollowElemContent (#27823) * fix: installation_id is missing when in tools page (#27849) * fix: avoid passing empty uniqueIdentifier to InstallFromMarketplace (#27802) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * test: create new test scripts and update some existing test scripts o… (#27850) * feat: change feedback to forum (#27862) * chore: translate i18n files and update type definitions (#27868) Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> * Fix/template transformer line number (#27867) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> * bump vite to 6.4.1 (#27877) * Add WEAVIATE_GRPC_ENDPOINT as designed in weaviate migration guide (#27861) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * Fix: correct DraftWorkflowApi.post response model (#27289) Signed-off-by: Yongtao Huang Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * fix Version 2.0.0-beta.2: Chat annotations Api Error #25506 (#27206) Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Asuka Minato * fix jina reader creadential migration command (#27883) * fix agent putout the output of workflow-tool twice (#26835) (#27087) * fix jina reader transform (#27922) * fix: prevent fetch version info in enterprise edition (#27923) * fix(api): fix `VariablePool.get` adding unexpected keys to variable_dictionary (#26767) Co-authored-by: -LAN- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * refactor: implement tenant self queue for rag tasks (#27559) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: -LAN- * fix: bump brotli to 1.2.0 resloved CVE-2025-6176 (#27950) Signed-off-by: kenwoodjw * docs: clarify how to obtain workflow_id for version execution (#28007) Signed-off-by: OneZero-Y * fix: fix https://github.com/langgenius/dify/issues/27939 (#27985) * fix: the model list encountered two children with the same key (#27956) Co-authored-by: haokai * add onupdate=func.current_timestamp() (#28014) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * chore(deps): bump scipy-stubs from 1.16.2.3 to 1.16.3.0 in /api (#28025) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Fix typo in weaviate comment, improve time test precision, and add security tests for get-icon utility (#27919) Signed-off-by: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * feat: Add Audio Content Support for MCP Tools (#27979) * fix: elasticsearch_vector version (#28028) Co-authored-by: huangzhuo Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * fix workflow default updated_at (#28047) * feat(api): Introduce Broadcast Channel (#27835) This PR introduces a `BroadcastChannel` abstraction with broadcasting and at-most once delivery semantics, serving as the communication component between celery worker and API server. It also includes a reference implementation backed by Redis PubSub. Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * Chore: change query log time range (#28052) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * tmp ci * fix * chore: Remove trailing space from migration filename (#28040) * chore: translate i18n files and update type definitions (#28054) Co-authored-by: iamjoel <2120155+iamjoel@users.noreply.github.com> * tmp ci * fix --------- Signed-off-by: Yongtao Huang Signed-off-by: kenwoodjw Signed-off-by: OneZero-Y Signed-off-by: dependabot[bot] Signed-off-by: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com> Co-authored-by: aka James4u Co-authored-by: Novice Co-authored-by: yangzheli <43645580+yangzheli@users.noreply.github.com> Co-authored-by: Elliott <105957288+Elliott-byte@users.noreply.github.com> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: johnny0120 Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Gritty_dev <101377478+codomposer@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: wangjifeng <163279492+kk-wangjifeng@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Boris Polonsky Co-authored-by: Yongtao Huang Co-authored-by: Cursx <33718736+Cursx@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Asuka Minato Co-authored-by: Jyong <76649700+JohnJyong@users.noreply.github.com> Co-authored-by: red_sun <56100962+redSun64@users.noreply.github.com> Co-authored-by: NFish Co-authored-by: QuantumGhost Co-authored-by: -LAN- Co-authored-by: hj24 Co-authored-by: kenwoodjw Co-authored-by: OneZero-Y Co-authored-by: wangxiaolei Co-authored-by: Kenn Co-authored-by: haokai Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com> Co-authored-by: Will Co-authored-by: huangzhuo1949 <167434202+huangzhuo1949@users.noreply.github.com> Co-authored-by: huangzhuo Co-authored-by: Joel Co-authored-by: iamjoel <2120155+iamjoel@users.noreply.github.com> --- ..._remove_extra_tracing_app_config_table.py} | 0 .../[appId]/overview/chart-view.tsx | 45 +++------- .../time-range-picker/date-picker.tsx | 80 +++++++++++++++++ .../overview/time-range-picker/index.tsx | 86 +++++++++++++++++++ .../time-range-picker/range-selector.tsx | 81 +++++++++++++++++ web/app/components/app/overview/app-chart.tsx | 15 +++- .../date-and-time-picker/calendar/index.tsx | 4 +- .../date-and-time-picker/calendar/item.tsx | 4 +- .../date-picker/index.tsx | 27 ++++-- .../base/date-and-time-picker/types.ts | 4 + .../assets/vender/other/hourglass-shape.svg | 3 + .../src/vender/other/HourglassShape.json | 27 ++++++ .../icons/src/vender/other/HourglassShape.tsx | 20 +++++ .../base/icons/src/vender/other/index.ts | 1 + web/app/components/base/select/index.tsx | 4 +- web/i18n/de-DE/app-log.ts | 2 + web/i18n/en-US/app-log.ts | 2 + web/i18n/es-ES/app-log.ts | 2 + web/i18n/fa-IR/app-log.ts | 2 + web/i18n/fr-FR/app-log.ts | 2 + web/i18n/hi-IN/app-log.ts | 2 + web/i18n/id-ID/app-log.ts | 2 + web/i18n/it-IT/app-log.ts | 2 + web/i18n/ja-JP/app-log.ts | 2 + web/i18n/ko-KR/app-log.ts | 2 + web/i18n/pl-PL/app-log.ts | 2 + web/i18n/pt-BR/app-log.ts | 2 + web/i18n/ro-RO/app-log.ts | 2 + web/i18n/ru-RU/app-log.ts | 2 + web/i18n/sl-SI/app-log.ts | 2 + web/i18n/th-TH/app-log.ts | 2 + web/i18n/tr-TR/app-log.ts | 2 + web/i18n/uk-UA/app-log.ts | 2 + web/i18n/vi-VN/app-log.ts | 2 + web/i18n/zh-Hans/app-log.ts | 2 + web/i18n/zh-Hant/app-log.ts | 2 + web/utils/format.ts | 51 +++++++++++ 37 files changed, 446 insertions(+), 48 deletions(-) rename api/migrations/versions/{fecff1c3da27_remove_extra_tracing_app_config_table .py => fecff1c3da27_remove_extra_tracing_app_config_table.py} (100%) create mode 100644 web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx create mode 100644 web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/index.tsx create mode 100644 web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx create mode 100644 web/app/components/base/icons/assets/vender/other/hourglass-shape.svg create mode 100644 web/app/components/base/icons/src/vender/other/HourglassShape.json create mode 100644 web/app/components/base/icons/src/vender/other/HourglassShape.tsx diff --git a/api/migrations/versions/fecff1c3da27_remove_extra_tracing_app_config_table .py b/api/migrations/versions/fecff1c3da27_remove_extra_tracing_app_config_table.py similarity index 100% rename from api/migrations/versions/fecff1c3da27_remove_extra_tracing_app_config_table .py rename to api/migrations/versions/fecff1c3da27_remove_extra_tracing_app_config_table.py diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chart-view.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chart-view.tsx index 847de19165..d5430b03ad 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chart-view.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chart-view.tsx @@ -5,15 +5,19 @@ import quarterOfYear from 'dayjs/plugin/quarterOfYear' import { useTranslation } from 'react-i18next' import type { PeriodParams } from '@/app/components/app/overview/app-chart' import { AvgResponseTime, AvgSessionInteractions, AvgUserInteractions, ConversationsChart, CostChart, EndUsersChart, MessagesChart, TokenPerSecond, UserSatisfactionRate, WorkflowCostChart, WorkflowDailyTerminalsChart, WorkflowMessagesChart } from '@/app/components/app/overview/app-chart' -import type { Item } from '@/app/components/base/select' -import { SimpleSelect } from '@/app/components/base/select' -import { TIME_PERIOD_MAPPING } from '@/app/components/app/log/filter' import { useStore as useAppStore } from '@/app/components/app/store' +import TimeRangePicker from './time-range-picker' dayjs.extend(quarterOfYear) const today = dayjs() +const TIME_PERIOD_MAPPING = [ + { value: 0, name: 'today' }, + { value: 7, name: 'last7days' }, + { value: 30, name: 'last30days' }, +] + const queryDateFormat = 'YYYY-MM-DD HH:mm' export type IChartViewProps = { @@ -26,21 +30,7 @@ export default function ChartView({ appId, headerRight }: IChartViewProps) { const appDetail = useAppStore(state => state.appDetail) const isChatApp = appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow' const isWorkflow = appDetail?.mode === 'workflow' - const [period, setPeriod] = useState({ name: t('appLog.filter.period.last7days'), query: { start: today.subtract(7, 'day').startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } }) - - const onSelect = (item: Item) => { - if (item.value === -1) { - setPeriod({ name: item.name, query: undefined }) - } - else if (item.value === 0) { - const startOfToday = today.startOf('day').format(queryDateFormat) - const endOfToday = today.endOf('day').format(queryDateFormat) - setPeriod({ name: item.name, query: { start: startOfToday, end: endOfToday } }) - } - else { - setPeriod({ name: item.name, query: { start: today.subtract(item.value as number, 'day').startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } }) - } - } + const [period, setPeriod] = useState({ name: t('appLog.filter.period.today'), query: { start: today.startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } }) if (!appDetail) return null @@ -50,20 +40,11 @@ export default function ChartView({ appId, headerRight }: IChartViewProps) {
{t('common.appMenus.overview')}
-
- ({ value: k, name: t(`appLog.filter.period.${v.name}`) }))} - className='mt-0 !w-40' - notClearable={true} - onSelect={(item) => { - const id = item.value - const value = TIME_PERIOD_MAPPING[id]?.value ?? '-1' - const name = item.name || t('appLog.filter.period.allTime') - onSelect({ value, name }) - }} - defaultValue={'2'} - /> -
+ {headerRight}
diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx new file mode 100644 index 0000000000..2bfdece433 --- /dev/null +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx @@ -0,0 +1,80 @@ +'use client' +import { RiCalendarLine } from '@remixicon/react' +import type { Dayjs } from 'dayjs' +import type { FC } from 'react' +import React, { useCallback } from 'react' +import cn from '@/utils/classnames' +import { formatToLocalTime } from '@/utils/format' +import { useI18N } from '@/context/i18n' +import Picker from '@/app/components/base/date-and-time-picker/date-picker' +import type { TriggerProps } from '@/app/components/base/date-and-time-picker/types' +import { noop } from 'lodash-es' +import dayjs from 'dayjs' + +type Props = { + start: Dayjs + end: Dayjs + onStartChange: (date?: Dayjs) => void + onEndChange: (date?: Dayjs) => void +} + +const today = dayjs() +const DatePicker: FC = ({ + start, + end, + onStartChange, + onEndChange, +}) => { + const { locale } = useI18N() + + const renderDate = useCallback(({ value, handleClickTrigger, isOpen }: TriggerProps) => { + return ( +
+ {value ? formatToLocalTime(value, locale, 'MMM D') : ''} +
+ ) + }, [locale]) + + const availableStartDate = end.subtract(30, 'day') + const startDateDisabled = useCallback((date: Dayjs) => { + if (date.isAfter(today, 'date')) + return true + return !((date.isAfter(availableStartDate, 'date') || date.isSame(availableStartDate, 'date')) && (date.isBefore(end, 'date') || date.isSame(end, 'date'))) + }, [availableStartDate, end]) + + const availableEndDate = start.add(30, 'day') + const endDateDisabled = useCallback((date: Dayjs) => { + if (date.isAfter(today, 'date')) + return true + return !((date.isAfter(start, 'date') || date.isSame(start, 'date')) && (date.isBefore(availableEndDate, 'date') || date.isSame(availableEndDate, 'date'))) + }, [availableEndDate, start]) + + return ( +
+
+ +
+ + - + +
+ + ) +} +export default React.memo(DatePicker) diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/index.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/index.tsx new file mode 100644 index 0000000000..4738bdeebf --- /dev/null +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/index.tsx @@ -0,0 +1,86 @@ +'use client' +import type { PeriodParams, PeriodParamsWithTimeRange } from '@/app/components/app/overview/app-chart' +import type { FC } from 'react' +import React, { useCallback, useState } from 'react' +import type { Dayjs } from 'dayjs' +import { HourglassShape } from '@/app/components/base/icons/src/vender/other' +import RangeSelector from './range-selector' +import DatePicker from './date-picker' +import dayjs from 'dayjs' +import { useI18N } from '@/context/i18n' +import { formatToLocalTime } from '@/utils/format' + +const today = dayjs() + +type Props = { + ranges: { value: number; name: string }[] + onSelect: (payload: PeriodParams) => void + queryDateFormat: string +} + +const TimeRangePicker: FC = ({ + ranges, + onSelect, + queryDateFormat, +}) => { + const { locale } = useI18N() + + const [isCustomRange, setIsCustomRange] = useState(false) + const [start, setStart] = useState(today) + const [end, setEnd] = useState(today) + + const handleRangeChange = useCallback((payload: PeriodParamsWithTimeRange) => { + setIsCustomRange(false) + setStart(payload.query!.start) + setEnd(payload.query!.end) + onSelect({ + name: payload.name, + query: { + start: payload.query!.start.format(queryDateFormat), + end: payload.query!.end.format(queryDateFormat), + }, + }) + }, [onSelect, queryDateFormat]) + + const handleDateChange = useCallback((type: 'start' | 'end') => { + return (date?: Dayjs) => { + if (!date) return + if (type === 'start' && date.isSame(start)) return + if (type === 'end' && date.isSame(end)) return + if (type === 'start') + setStart(date) + else + setEnd(date) + + const currStart = type === 'start' ? date : start + const currEnd = type === 'end' ? date : end + onSelect({ + name: `${formatToLocalTime(currStart, locale, 'MMM D')} - ${formatToLocalTime(currEnd, locale, 'MMM D')}`, + query: { + start: currStart.format(queryDateFormat), + end: currEnd.format(queryDateFormat), + }, + }) + + setIsCustomRange(true) + } + }, [start, end, onSelect, locale, queryDateFormat]) + + return ( +
+ + + +
+ ) +} +export default React.memo(TimeRangePicker) diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx new file mode 100644 index 0000000000..f99ea52492 --- /dev/null +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx @@ -0,0 +1,81 @@ +'use client' +import type { PeriodParamsWithTimeRange, TimeRange } from '@/app/components/app/overview/app-chart' +import type { FC } from 'react' +import React, { useCallback } from 'react' +import { SimpleSelect } from '@/app/components/base/select' +import type { Item } from '@/app/components/base/select' +import dayjs from 'dayjs' +import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react' +import cn from '@/utils/classnames' +import { useTranslation } from 'react-i18next' + +const today = dayjs() + +type Props = { + isCustomRange: boolean + ranges: { value: number; name: string }[] + onSelect: (payload: PeriodParamsWithTimeRange) => void +} + +const RangeSelector: FC = ({ + isCustomRange, + ranges, + onSelect, +}) => { + const { t } = useTranslation() + + const handleSelectRange = useCallback((item: Item) => { + const { name, value } = item + let period: TimeRange | null = null + if (value === 0) { + const startOfToday = today.startOf('day') + const endOfToday = today.endOf('day') + period = { start: startOfToday, end: endOfToday } + } + else { + period = { start: today.subtract(item.value as number, 'day').startOf('day'), end: today.endOf('day') } + } + onSelect({ query: period!, name }) + }, [onSelect]) + + const renderTrigger = useCallback((item: Item | null, isOpen: boolean) => { + return ( +
+
{isCustomRange ? t('appLog.filter.period.custom') : item?.name}
+ +
+ ) + }, [isCustomRange]) + + const renderOption = useCallback(({ item, selected }: { item: Item; selected: boolean }) => { + return ( + <> + {selected && ( + + + )} + {item.name} + + ) + }, []) + return ( + ({ ...v, name: t(`appLog.filter.period.${v.name}`) }))} + className='mt-0 !w-40' + notClearable={true} + onSelect={handleSelectRange} + defaultValue={0} + wrapperClassName='h-8' + optionWrapClassName='w-[200px] translate-x-[-24px]' + renderTrigger={renderTrigger} + optionClassName='flex items-center py-0 pl-7 pr-2 h-8' + renderOption={renderOption} + /> + ) +} +export default React.memo(RangeSelector) diff --git a/web/app/components/app/overview/app-chart.tsx b/web/app/components/app/overview/app-chart.tsx index c550f0b23f..8f28e16402 100644 --- a/web/app/components/app/overview/app-chart.tsx +++ b/web/app/components/app/overview/app-chart.tsx @@ -4,6 +4,7 @@ import React from 'react' import ReactECharts from 'echarts-for-react' import type { EChartsOption } from 'echarts' import useSWR from 'swr' +import type { Dayjs } from 'dayjs' import dayjs from 'dayjs' import { get } from 'lodash-es' import Decimal from 'decimal.js' @@ -78,6 +79,16 @@ export type PeriodParams = { } } +export type TimeRange = { + start: Dayjs + end: Dayjs +} + +export type PeriodParamsWithTimeRange = { + name: string + query?: TimeRange +} + export type IBizChartProps = { period: PeriodParams id: string @@ -215,9 +226,7 @@ const Chart: React.FC = ({ formatter(params) { return `
${params.name}
${valueFormatter((params.data as any)[yField])} - ${!CHART_TYPE_CONFIG[chartType].showTokens - ? '' - : ` + ${!CHART_TYPE_CONFIG[chartType].showTokens ? '' : ` ( ~$${get(params.data, 'total_price', 0)} ) diff --git a/web/app/components/base/date-and-time-picker/calendar/index.tsx b/web/app/components/base/date-and-time-picker/calendar/index.tsx index 00612fcb37..03dcb0eda3 100644 --- a/web/app/components/base/date-and-time-picker/calendar/index.tsx +++ b/web/app/components/base/date-and-time-picker/calendar/index.tsx @@ -8,9 +8,10 @@ const Calendar: FC = ({ selectedDate, onDateClick, wrapperClassName, + getIsDateDisabled, }) => { return
- +
{ days.map(day => = ({ day={day} selectedDate={selectedDate} onClick={onDateClick} + isDisabled={getIsDateDisabled ? getIsDateDisabled(day.date) : false} />) }
diff --git a/web/app/components/base/date-and-time-picker/calendar/item.tsx b/web/app/components/base/date-and-time-picker/calendar/item.tsx index 1da8b9b3b5..7132d7bdfb 100644 --- a/web/app/components/base/date-and-time-picker/calendar/item.tsx +++ b/web/app/components/base/date-and-time-picker/calendar/item.tsx @@ -7,6 +7,7 @@ const Item: FC = ({ day, selectedDate, onClick, + isDisabled, }) => { const { date, isCurrentMonth } = day const isSelected = selectedDate?.isSame(date, 'date') @@ -14,11 +15,12 @@ const Item: FC = ({ return (