ronantakizawa/codereview-bench huggingface dataset:
Repository: moeru-ai/airi
File: packages/stage-ui/src/components/Scenes/Live2D.vue (Vue)

Code before:
withDefaults(defineProps<{
  modelSrc?: string

  paused?: boolean
  mouthOpenSize?: number
  focusAt?: { x: number, y: number }
  disableFocusAt?: boolean
  xOffset?: number | string
  yOffset?: number | string
  scale?: number
}>(), {
  paused: false,
  focusAt: () => ({ x: 0, y: 0 }),
  mouthOpenSize: 0,
  scale: 1,
})

const live2dCanvasRef = ref<{ canvasElement: () => HTMLCanvasElement }>()

function canvasElement() {
  return live2dCanvasRef.value?.canvasElement()
}

defineExpose({
  canvasElement,
})
</script>

<template>
  <Screen v-slot="{ width, height }" relative>
    <Live2DCanvas
      ref="live2dCanvasRef"
      v-slot="{ app }"
      :width="width"
      :height="height"
      :resolution="2"
      max-h="100dvh"
    >
      <Live2DModel
        :model-src="modelSrc"
        :app="app"
        :mouth-open-size="mouthOpenSize"
        :width="width"
        :height="height"
        :paused="paused"
        :focus-at="focusAt"
        :x-offset="xOffset"
        :y-offset="yOffset"
        :scale="scale"
        :disable-focus-at="disableFocusAt"
      />

Review comment (suggestion):
```suggestion
defineExpose({
  canvasElement: () => {
    return live2dCanvasRef.value?.canvasElement()
  },
})
```

Code after:
withDefaults(defineProps<{
  modelSrc?: string

  paused?: boolean
  mouthOpenSize?: number
  focusAt?: { x: number, y: number }
  disableFocusAt?: boolean
  xOffset?: number | string
  yOffset?: number | string
  scale?: number
}>(), {
  paused: false,
  focusAt: () => ({ x: 0, y: 0 }),
  mouthOpenSize: 0,
  scale: 1,
})

const live2dCanvasRef = ref<InstanceType<typeof Live2DCanvas>>()

defineExpose({
  canvasElement: () => {
    return live2dCanvasRef.value?.canvasElement()
  },
})
</script>

<template>
  <Screen v-slot="{ width, height }" relative>
    <Live2DCanvas
      ref="live2dCanvasRef"
      v-slot="{ app }"
      :width="width"
      :height="height"
      :resolution="2"
      max-h="100dvh"
    >
      <Live2DModel
        :model-src="modelSrc"
        :app="app"
        :mouth-open-size="mouthOpenSize"
        :width="width"
        :height="height"
        :paused="paused"
        :focus-at="focusAt"
        :x-offset="xOffset"
        :y-offset="yOffset"
        :scale="scale"
        :disable-focus-at="disableFocusAt"
      />
    </Live2DCanvas>
  </Screen>
Repository: moeru-ai/airi
File: packages/stage-ui/src/components/Scenes/Live2D.vue (Vue)

Code before:
import { ref } from 'vue'

import Screen from '../Misc/Screen.vue'
import Live2DCanvas from './Live2D/Canvas.vue'
import Live2DModel from './Live2D/Model.vue'

import '../../utils/live2d-zip-loader'

withDefaults(defineProps<{
  modelSrc?: string

  paused?: boolean
  mouthOpenSize?: number
  focusAt?: { x: number, y: number }
  disableFocusAt?: boolean
  xOffset?: number | string
  yOffset?: number | string
  scale?: number
}>(), {
  paused: false,
  focusAt: () => ({ x: 0, y: 0 }),
  mouthOpenSize: 0,
  scale: 1,
})

const live2dCanvasRef = ref<{ canvasElement: () => HTMLCanvasElement }>()

defineExpose({
  canvasElement: () => {
    return live2dCanvasRef.value?.canvasElement()
  },
})
</script>

<template>
  <Screen v-slot="{ width, height }" relative>
    <Live2DCanvas
      ref="live2dCanvasRef"
      v-slot="{ app }"
      :width="width"
      :height="height"
      :resolution="2"
      max-h="100dvh"
    >
      <Live2DModel
        :model-src="modelSrc"
        :app="app"
        :mouth-open-size="mouthOpenSize"
        :width="width"
        :height="height"
        :paused="paused"

Review comment (suggestion):
```suggestion
const live2dCanvasRef = ref<InstanceType<typeof Live2DCanvas>>()
```

Code after:
import { ref } from 'vue'

import Screen from '../Misc/Screen.vue'
import Live2DCanvas from './Live2D/Canvas.vue'
import Live2DModel from './Live2D/Model.vue'

import '../../utils/live2d-zip-loader'

withDefaults(defineProps<{
  modelSrc?: string

  paused?: boolean
  mouthOpenSize?: number
  focusAt?: { x: number, y: number }
  disableFocusAt?: boolean
  xOffset?: number | string
  yOffset?: number | string
  scale?: number
}>(), {
  paused: false,
  focusAt: () => ({ x: 0, y: 0 }),
  mouthOpenSize: 0,
  scale: 1,
})

const live2dCanvasRef = ref<InstanceType<typeof Live2DCanvas>>()

defineExpose({
  canvasElement: () => {
    return live2dCanvasRef.value?.canvasElement()
  },
})
</script>

<template>
  <Screen v-slot="{ width, height }" relative>
    <Live2DCanvas
      ref="live2dCanvasRef"
      v-slot="{ app }"
      :width="width"
      :height="height"
      :resolution="2"
      max-h="100dvh"
    >
      <Live2DModel
        :model-src="modelSrc"
        :app="app"
        :mouth-open-size="mouthOpenSize"
        :width="width"
        :height="height"
        :paused="paused"
Repository: moeru-ai/airi
File: packages/stage-ui/src/components/Scenes/Stage.vue (Vue)

Code before:
})

vrmStore.onShouldUpdateView(async () => {
  showStage.value = false
  await settingsStore.updateStageModel()
  setTimeout(() => {
    showStage.value = true
  }, 100)
})

const audioAnalyser = ref<AnalyserNode>()
const nowSpeaking = ref(false)
const lipSyncStarted = ref(false)
let currentAudioSource: AudioBufferSourceNode | null = null

const audioQueue = useQueue<{ audioBuffer: AudioBuffer, text: string }>({
  handlers: [
    (ctx) => {
      return new Promise((resolve) => {
        // Stop any currently playing audio
        if (currentAudioSource) {
          try {
            currentAudioSource.stop()
          }
          catch {}
          currentAudioSource.disconnect()
          currentAudioSource = null
        }
        // Create an AudioBufferSourceNode
        const source = audioContext.createBufferSource()
        source.buffer = ctx.data.audioBuffer

        // Connect the source to the AudioContext's destination (the speakers)
        source.connect(audioContext.destination)
        // Connect the source to the analyzer
        source.connect(audioAnalyser.value!)

        // Start playing the audio
        nowSpeaking.value = true
        currentAudioSource = source
        source.start(0)
        source.onended = () => {
          nowSpeaking.value = false
          if (currentAudioSource === source) {
            currentAudioSource = null
          }
          resolve()
        }
      })
    },
  ],

Review comment (suggestion):
Shall we move `disconnect` into try-catch as well?

```suggestion
            currentAudioSource.disconnect()
          }
          catch {}
```

Code after:
})

vrmStore.onShouldUpdateView(async () => {
  showStage.value = false
  await settingsStore.updateStageModel()
  setTimeout(() => {
    showStage.value = true
  }, 100)
})

const audioAnalyser = ref<AnalyserNode>()
const nowSpeaking = ref(false)
const lipSyncStarted = ref(false)
let currentAudioSource: AudioBufferSourceNode | null = null

const audioQueue = useQueue<{ audioBuffer: AudioBuffer, text: string }>({
  handlers: [
    (ctx) => {
      return new Promise((resolve) => {
        // Stop any currently playing audio
        if (currentAudioSource) {
          try {
            currentAudioSource.stop()
            currentAudioSource.disconnect()
          }
          catch {}
          currentAudioSource = null
        }
        // Create an AudioBufferSourceNode
        const source = audioContext.createBufferSource()
        source.buffer = ctx.data.audioBuffer

        // Connect the source to the AudioContext's destination (the speakers)
        source.connect(audioContext.destination)
        // Connect the source to the analyzer
        source.connect(audioAnalyser.value!)

        // Start playing the audio
        nowSpeaking.value = true
        currentAudioSource = source
        source.start(0)
        source.onended = () => {
          nowSpeaking.value = false
          if (currentAudioSource === source) {
            currentAudioSource = null
          }
          resolve()
        }
      })
    },
  ],
Repository: moeru-ai/airi
File: packages/stage-ui/src/components/Scenes/Stage.vue (Vue)

Code before:
    return

  mouthOpenSize.value = calculateVolume(audioAnalyser.value!, 'linear')
}

function setupLipSync() {
  if (!lipSyncStarted.value) {
    getVolumeWithMinMaxNormalizeWithFrameUpdates()
    audioContext.resume()
    lipSyncStarted.value = true
  }
}

function setupAnalyser() {
  if (!audioAnalyser.value)
    audioAnalyser.value = audioContext.createAnalyser()
}

onBeforeMessageComposed(async () => {
  // Stop any currently playing audio and clear the audio queue
  if (currentAudioSource) {
    try {
      currentAudioSource.stop()
    }
    catch {}
    currentAudioSource.disconnect()
    currentAudioSource = null
  }
  audioQueue.queue.value = []
  setupAnalyser()
  setupLipSync()
})

onBeforeSend(async () => {
  currentMotion.value = { group: EmotionThinkMotionName }
})

onTokenLiteral(async (literal) => {
  await messageContentQueue.add(literal)
})

onTokenSpecial(async (special) => {
  await delaysQueue.add(special)
  await emotionMessageContentQueue.add(special)
})

onStreamEnd(async () => {
  await delaysQueue.add(llmInferenceEndToken)
})

onAssistantResponseEnd(async (_message) => {

Review comment (suggestion):
```suggestion
      currentAudioSource.disconnect()
    }
    catch {}
```

Code after:
    return

  mouthOpenSize.value = calculateVolume(audioAnalyser.value!, 'linear')
}

function setupLipSync() {
  if (!lipSyncStarted.value) {
    getVolumeWithMinMaxNormalizeWithFrameUpdates()
    audioContext.resume()
    lipSyncStarted.value = true
  }
}

function setupAnalyser() {
  if (!audioAnalyser.value)
    audioAnalyser.value = audioContext.createAnalyser()
}

onBeforeMessageComposed(async () => {
  // Stop any currently playing audio and clear the audio queue
  if (currentAudioSource) {
    try {
      currentAudioSource.stop()
      currentAudioSource.disconnect()
    }
    catch {}
    currentAudioSource = null
  }
  audioQueue.queue.value = []
  setupAnalyser()
  setupLipSync()
})

onBeforeSend(async () => {
  currentMotion.value = { group: EmotionThinkMotionName }
})

onTokenLiteral(async (literal) => {
  await messageContentQueue.add(literal)
})

onTokenSpecial(async (special) => {
  await delaysQueue.add(special)
  await emotionMessageContentQueue.add(special)
})

onStreamEnd(async () => {
  await delaysQueue.add(llmInferenceEndToken)
})

onAssistantResponseEnd(async (_message) => {
Repository: moeru-ai/airi
File: apps/stage-tamagotchi/src/main/index.ts (TypeScript)

Code before:
    },
    { type: 'separator' },
    {
      label: 'Settings',
      click: () => {
        if (mainWindow) {
          showMainWindow()
          // Send the open settings command using the pre-created invoker
          if (openSettingsInvoker) {
            openSettingsInvoker(undefined)
          }
        }
      },
    },
    { type: 'separator' },
    {
      label: 'Quit',
      click: () => {
        app.quit()
      },
    },
  ])

  // Set tray properties
  appTray.setContextMenu(contextMenu)
  appTray.setToolTip('AIRI - AI Virtual Assistant')
  appTray.addListener('click', showMainWindow)

  // On macOS, there's a special double-click event
  if (platform === 'darwin') {
    appTray.addListener('double-click', showMainWindow)
  }
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
  if (/^true$/i.test(env.APP_REMOTE_DEBUG || '')) {
    const remoteDebugEndpoint = `http://localhost:${env.APP_REMOTE_DEBUG_PORT || '9222'}`

    http.get(`${remoteDebugEndpoint}/json`, (res) => {
      let data = ''
      res.on('data', chunk => data += chunk)
      res.on('end', () => {
        try {
          const targets = JSON.parse(data)
          if (targets.length <= 0) {
            console.warn('[Remote Debugging] No targets found')
            return
          }

Review comment (suggestion):
```suggestion
  appTray.setToolTip('Project AIRI')
```

Code after:
    },
    { type: 'separator' },
    {
      label: 'Settings',
      click: () => {
        if (mainWindow) {
          showMainWindow()
          // Send the open settings command using the pre-created invoker
          if (openSettingsInvoker) {
            openSettingsInvoker(undefined)
          }
        }
      },
    },
    { type: 'separator' },
    {
      label: 'Quit',
      click: () => {
        app.quit()
      },
    },
  ])

  // Set tray properties
  appTray.setContextMenu(contextMenu)
  appTray.setToolTip('Project AIRI')
  appTray.addListener('click', showMainWindow)

  // On macOS, there's a special double-click event
  if (platform === 'darwin') {
    appTray.addListener('double-click', showMainWindow)
  }
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
  if (/^true$/i.test(env.APP_REMOTE_DEBUG || '')) {
    const remoteDebugEndpoint = `http://localhost:${env.APP_REMOTE_DEBUG_PORT || '9222'}`

    http.get(`${remoteDebugEndpoint}/json`, (res) => {
      let data = ''
      res.on('data', chunk => data += chunk)
      res.on('end', () => {
        try {
          const targets = JSON.parse(data)
          if (targets.length <= 0) {
            console.warn('[Remote Debugging] No targets found')
            return
          }
Repository: moeru-ai/airi
File: packages/ui/src/components/Form/Field/FieldRange.vue (Vue)

Code before:
<script setup lang="ts">
import Range from '../Range/Range.vue'

const props = withDefaults(defineProps<{
  min?: number
  max?: number
  step?: number
  label?: string
  description?: string
  formatValue?: (value: number) => string
  as?: 'label' | 'div'
}>(), {
  as: 'label',
})

const modelValue = defineModel<number>({ required: true })
</script>

<template>
  <props.as :class="['flex', 'flex-col', 'gap-4']">
    <div :class="['flex', 'flex-row', 'items-center', 'gap-2']">
      <div :class="['flex-1']">
        <div :class="['flex', 'items-center', 'gap-1', 'text-sm', 'font-medium']">
          <slot name="label">
            {{ label }}
          </slot>
        </div>
        <div :class="['text-xs', 'text-neutral-500', 'dark:text-neutral-400']">
          <slot name="description">
            {{ description }}
          </slot>
        </div>
      </div>
      <span :class="['font-mono']">{{ props.formatValue?.(modelValue) || modelValue }}</span>
    </div>
    <div :class="['flex', 'flex-row', 'items-center', 'gap-2']">
      <Range
        v-model="modelValue"
        :min="min || 0"
        :max="max || 1"
        :step="step || 0.01"
        :class="['w-full']"
      />
    </div>
  </props.as>

Review comment (suggestion):
```suggestion
  <props.as :class="['flex flex-col gap-4']">
```

They are in the same group, no need to separate to multiple strings.

Code after:
<script setup lang="ts">
import Range from '../Range/Range.vue'

const props = withDefaults(defineProps<{
  min?: number
  max?: number
  step?: number
  label?: string
  description?: string
  formatValue?: (value: number) => string
  as?: 'label' | 'div'
}>(), {
  as: 'label',
})

const modelValue = defineModel<number>({ required: true })
</script>

<template>
  <props.as :class="['flex flex-col gap-4']">
    <div :class="['flex', 'flex-row', 'items-center', 'gap-2']">
      <div :class="['flex-1']">
        <div :class="['flex', 'items-center', 'gap-1', 'text-sm', 'font-medium']">
          <slot name="label">
            {{ label }}
          </slot>
        </div>
        <div :class="['text-xs', 'text-neutral-500', 'dark:text-neutral-400']">
          <slot name="description">
            {{ description }}
          </slot>
        </div>
      </div>
      <span :class="['font-mono']">{{ props.formatValue?.(modelValue) || modelValue }}</span>
    </div>
    <div :class="['flex', 'flex-row', 'items-center', 'gap-2']">
      <Range
        v-model="modelValue"
        :min="min || 0"
        :max="max || 1"
        :step="step || 0.01"
        :class="['w-full']"
      />
    </div>
  </props.as>
Repository: moeru-ai/airi
File: packages/ui/src/components/Form/Input/BasicInputFile.vue (Vue)

Code before:
<script setup lang="ts">
import { useDebounce } from '@vueuse/core'
import { ref } from 'vue'

const props = defineProps<{
  class?: string | string[] | null
  isDraggingClasses?: string | string[] | null
  isNotDraggingClasses?: string | string[] | null
  accept?: string
  multiple?: boolean
  modelValue?: File[]
}>()

const emit = defineEmits<{
  'update:modelValue': [files: File[]]
}>()

const files = ref<File[]>([])
const firstFile = ref<File>()

const isDragging = ref(false)
const isDraggingDebounced = useDebounce(isDragging, 150)

function handleFileChange(e: Event) {
  const input = e.target as HTMLInputElement
  if (!input.files)
    return

  files.value = []

  for (let i = 0; i < input.files.length; i++) {
    files.value.push(input.files[i])
  }

  emit('update:modelValue', files.value)

  if (files.value.length > 0) {
    firstFile.value = files.value[0]
  }

  isDragging.value = false
}
</script>

Review comment (suggestion):
```suggestion
const files = ref<File[]>(props.modelValue || [])
```

Code after:
<script setup lang="ts">
import { useDebounce } from '@vueuse/core'
import { ref } from 'vue'

const props = defineProps<{
  class?: string | string[] | null
  isDraggingClasses?: string | string[] | null
  isNotDraggingClasses?: string | string[] | null
  accept?: string
  multiple?: boolean
  modelValue?: File[]
}>()

const emit = defineEmits<{
  'update:modelValue': [files: File[]]
}>()

const files = ref<File[]>([])
const firstFile = ref<File>()

const isDragging = ref(false)
const isDraggingDebounced = useDebounce(isDragging, 150)

function handleFileChange(e: Event) {
  const input = e.target as HTMLInputElement
  if (!input.files)
    return

  files.value = []

  for (let i = 0; i < input.files.length; i++) {
    files.value.push(input.files[i])
  }

  emit('update:modelValue', files.value)

  if (files.value.length > 0) {
    firstFile.value = files.value[0]
  }

  isDragging.value = false

  // Allow re-selecting the same file
Repository: moeru-ai/airi
File: packages/stage-ui/src/utils/live2d-zip-loader.ts (TypeScript)

Code before:
import type { ModelSettings } from 'pixi-live2d-display/cubism4'

import JSZip from 'jszip'

import { Cubism4ModelSettings, ModelSettings as ModelSettingsClass, ZipLoader } from 'pixi-live2d-display/cubism4'

// NOTICE: Patch to handle UTF-8 encoded file paths in model settings
const originalValidateFiles = ModelSettingsClass.prototype.validateFiles
ModelSettingsClass.prototype.validateFiles = function (files: string[]): string[] {
  const normalizedFiles = files.map((f) => {
    try {
      return decodeURI(f)
    }
    catch {
      // Not a valid URI, return original string.
      return f
    }
  })
  return originalValidateFiles.call(this, normalizedFiles)
}

ZipLoader.zipReader = (data: Blob, _url: string) => JSZip.loadAsync(data)

const defaultCreateSettings = ZipLoader.createSettings
ZipLoader.createSettings = async (reader: JSZip) => {
  const filePaths = Object.keys(reader.files)

  if (!filePaths.find(file => isSettingsFile(file))) {
    return createFakeSettings(filePaths)
  }

  return defaultCreateSettings(reader)
}

Review comment (suggestion):
```suggestion
// TODO: this is a bit hacky, do we have other choice?
// NOTICE: Patch to handle UTF-8 encoded file paths in model settings
```

Code after:
import type { ModelSettings } from 'pixi-live2d-display/cubism4'

import JSZip from 'jszip'

import { Cubism4ModelSettings, ModelSettings as ModelSettingsClass, ZipLoader } from 'pixi-live2d-display/cubism4'

// TODO: this is a bit hacky, do we have other choice?
// NOTICE: Patch to handle UTF-8 encoded file paths in model settings
const originalValidateFiles = ModelSettingsClass.prototype.validateFiles
ModelSettingsClass.prototype.validateFiles = function (files: string[]): string[] {
  const normalizedFiles = files.map((f) => {
    try {
      return decodeURI(f)
    }
    catch {
      // Not a valid URI, return original string.
      return f
    }
  })
  return originalValidateFiles.call(this, normalizedFiles)
}

ZipLoader.zipReader = (data: Blob, _url: string) => JSZip.loadAsync(data)

const defaultCreateSettings = ZipLoader.createSettings
ZipLoader.createSettings = async (reader: JSZip) => {
  const filePaths = Object.keys(reader.files)

  if (!filePaths.find(file => isSettingsFile(file))) {
    return createFakeSettings(filePaths)
  }

  return defaultCreateSettings(reader)
Repository: moeru-ai/airi
File: apps/stage-tamagotchi/src/renderer/stores/controls-island.ts (TypeScript)

Code before:
import { useLocalStorage } from '@vueuse/core'
import { defineStore } from 'pinia'

export const useControlsIslandStore = defineStore('controls-island', () => {
  // Persist fade-on-hover preference per user
  const fadeOnHoverEnabled = useLocalStorage<boolean>('controls-island/fade-on-hover-enabled', false)
  const fadeOnHoverNoticeDontShowAgain = useLocalStorage<boolean>('notice/fade-on-hover/dont-show-again', false)

  function enableFadeOnHover() {
    fadeOnHoverEnabled.value = true
  }

  function disableFadeOnHover() {
    fadeOnHoverEnabled.value = false
  }

  return {
    fadeOnHoverEnabled,
    fadeOnHoverNoticeDontShowAgain,
    enableFadeOnHover,
    disableFadeOnHover,
  }
})


Review comment (suggestion):
```suggestion
  const dontShowItAgainNoticeFadeOnHover = useLocalStorage<boolean>('preferences/dont-show-it-again/notice/fade-on-hover', false)
```

Code after:
import { useLocalStorage } from '@vueuse/core'
import { defineStore } from 'pinia'

export const useControlsIslandStore = defineStore('controls-island', () => {
  // Persist fade-on-hover preference per user
  const fadeOnHoverEnabled = useLocalStorage<boolean>('controls-island/fade-on-hover-enabled', false)
  const dontShowItAgainNoticeFadeOnHover = useLocalStorage<boolean>('preferences/dont-show-it-again/notice/fade-on-hover', false)

  function enableFadeOnHover() {
    fadeOnHoverEnabled.value = true
  }

  function disableFadeOnHover() {
    fadeOnHoverEnabled.value = false
  }

  return {
    fadeOnHoverEnabled,
    dontShowItAgainNoticeFadeOnHover,
    enableFadeOnHover,
    disableFadeOnHover,
  }
})
Repository: moeru-ai/airi
File: apps/stage-tamagotchi/src/renderer/stores/controls-island.ts (TypeScript)

Code before:
import { useLocalStorage } from '@vueuse/core'
import { defineStore } from 'pinia'

export const useControlsIslandStore = defineStore('controls-island', () => {
  // Persist fade-on-hover preference per user
  const fadeOnHoverEnabled = useLocalStorage<boolean>('controls-island/fade-on-hover-enabled', false)
  const fadeOnHoverNoticeDontShowAgain = useLocalStorage<boolean>('notice/fade-on-hover/dont-show-again', false)

  function enableFadeOnHover() {
    fadeOnHoverEnabled.value = true
  }

  function disableFadeOnHover() {
    fadeOnHoverEnabled.value = false
  }

  return {
    fadeOnHoverEnabled,
    fadeOnHoverNoticeDontShowAgain,
    enableFadeOnHover,
    disableFadeOnHover,
  }
})


Review comment (suggestion):
```suggestion
    dontShowItAgainNoticeFadeOnHover,
```

Code after:
import { useLocalStorage } from '@vueuse/core'
import { defineStore } from 'pinia'

export const useControlsIslandStore = defineStore('controls-island', () => {
  // Persist fade-on-hover preference per user
  const fadeOnHoverEnabled = useLocalStorage<boolean>('controls-island/fade-on-hover-enabled', false)
  const dontShowItAgainNoticeFadeOnHover = useLocalStorage<boolean>('preferences/dont-show-it-again/notice/fade-on-hover', false)

  function enableFadeOnHover() {
    fadeOnHoverEnabled.value = true
  }

  function disableFadeOnHover() {
    fadeOnHoverEnabled.value = false
  }

  return {
    fadeOnHoverEnabled,
    dontShowItAgainNoticeFadeOnHover,
    enableFadeOnHover,
    disableFadeOnHover,
  }
})