cd-aichat 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +188 -0
- package/dist/ailogo.png +0 -0
- package/dist/cd-aichat.css +1 -0
- package/dist/cd-aichat.es.js +32223 -0
- package/dist/cd-aichat.umd.js +279 -0
- package/dist/index.css +1 -0
- package/package.json +61 -0
- package/src/ailogo.png +0 -0
- package/src/components/AiChat.test.js +538 -0
- package/src/components/AiChat.vue +2206 -0
- package/src/components/AiChatWidget.test.js +312 -0
- package/src/components/AiChatWidget.vue +963 -0
- package/src/components/BurnAfterReadDialog.test.js +121 -0
- package/src/components/BurnAfterReadDialog.vue +511 -0
- package/src/components/BurnAfterReadMessage.test.js +188 -0
- package/src/components/BurnAfterReadMessage.vue +193 -0
- package/src/components/BurnIndicator.test.js +101 -0
- package/src/components/BurnIndicator.vue +164 -0
- package/src/components/ChatWindow.vue +0 -0
- package/src/components/MentionList.vue +163 -0
- package/src/components/ResourceList.vue +194 -0
- package/src/components/RichTextEditor.vue +437 -0
- package/src/components/ScheduledSendDialog.vue +476 -0
- package/src/components/ScreenshotOverlay.vue +78 -0
- package/src/components/SendButtonGroup.test.js +174 -0
- package/src/components/SendButtonGroup.vue +166 -0
- package/src/components/UserDrawer.vue +0 -0
- package/src/components/screenshot/ScreenshotsBackground/getBoundsByPoints.ts +41 -0
- package/src/components/screenshot/ScreenshotsBackground/index.scss +27 -0
- package/src/components/screenshot/ScreenshotsBackground/index.tsx +145 -0
- package/src/components/screenshot/ScreenshotsButton/index.scss +29 -0
- package/src/components/screenshot/ScreenshotsButton/index.tsx +58 -0
- package/src/components/screenshot/ScreenshotsCanvas/getBoundsByPoints.ts +55 -0
- package/src/components/screenshot/ScreenshotsCanvas/getPoints.ts +60 -0
- package/src/components/screenshot/ScreenshotsCanvas/index.scss +84 -0
- package/src/components/screenshot/ScreenshotsCanvas/index.tsx +277 -0
- package/src/components/screenshot/ScreenshotsCanvas/isPointInDraw.ts +35 -0
- package/src/components/screenshot/ScreenshotsColor/index.scss +45 -0
- package/src/components/screenshot/ScreenshotsColor/index.tsx +39 -0
- package/src/components/screenshot/ScreenshotsContext.ts +56 -0
- package/src/components/screenshot/ScreenshotsMagnifier/index.scss +61 -0
- package/src/components/screenshot/ScreenshotsMagnifier/index.tsx +126 -0
- package/src/components/screenshot/ScreenshotsOperations/index.scss +25 -0
- package/src/components/screenshot/ScreenshotsOperations/index.tsx +118 -0
- package/src/components/screenshot/ScreenshotsOption/index.scss +50 -0
- package/src/components/screenshot/ScreenshotsOption/index.tsx +150 -0
- package/src/components/screenshot/ScreenshotsSize/index.scss +28 -0
- package/src/components/screenshot/ScreenshotsSize/index.tsx +41 -0
- package/src/components/screenshot/ScreenshotsSizeColor/index.scss +8 -0
- package/src/components/screenshot/ScreenshotsSizeColor/index.tsx +25 -0
- package/src/components/screenshot/ScreenshotsTextarea/calculateNodeSize.ts +117 -0
- package/src/components/screenshot/ScreenshotsTextarea/index.scss +19 -0
- package/src/components/screenshot/ScreenshotsTextarea/index.tsx +96 -0
- package/src/components/screenshot/composeImage.ts +57 -0
- package/src/components/screenshot/exports.ts +4 -0
- package/src/components/screenshot/hooks/useBounds.ts +35 -0
- package/src/components/screenshot/hooks/useCall.ts +17 -0
- package/src/components/screenshot/hooks/useCanvasContextRef.ts +8 -0
- package/src/components/screenshot/hooks/useCanvasMousedown.ts +13 -0
- package/src/components/screenshot/hooks/useCanvasMousemove.ts +13 -0
- package/src/components/screenshot/hooks/useCanvasMouseup.ts +13 -0
- package/src/components/screenshot/hooks/useCursor.ts +34 -0
- package/src/components/screenshot/hooks/useDispatcher.ts +8 -0
- package/src/components/screenshot/hooks/useDrawSelect.ts +16 -0
- package/src/components/screenshot/hooks/useEmiter.ts +61 -0
- package/src/components/screenshot/hooks/useHistory.ts +160 -0
- package/src/components/screenshot/hooks/useLang.ts +8 -0
- package/src/components/screenshot/hooks/useOperation.ts +37 -0
- package/src/components/screenshot/hooks/useReset.ts +26 -0
- package/src/components/screenshot/hooks/useStore.ts +8 -0
- package/src/components/screenshot/icons/iconfont.scss +88 -0
- package/src/components/screenshot/icons/iconfont.ttf +0 -0
- package/src/components/screenshot/icons/iconfont.woff +0 -0
- package/src/components/screenshot/icons/iconfont.woff2 +0 -0
- package/src/components/screenshot/index.tsx +169 -0
- package/src/components/screenshot/operations/Arrow/draw.ts +56 -0
- package/src/components/screenshot/operations/Arrow/index.tsx +193 -0
- package/src/components/screenshot/operations/Brush/draw.ts +45 -0
- package/src/components/screenshot/operations/Brush/index.tsx +169 -0
- package/src/components/screenshot/operations/Cancel/index.tsx +18 -0
- package/src/components/screenshot/operations/Ellipse/draw.ts +96 -0
- package/src/components/screenshot/operations/Ellipse/index.tsx +245 -0
- package/src/components/screenshot/operations/Mosaic/index.tsx +223 -0
- package/src/components/screenshot/operations/Ok/index.tsx +37 -0
- package/src/components/screenshot/operations/Pin/index.tsx +37 -0
- package/src/components/screenshot/operations/Rectangle/draw.ts +80 -0
- package/src/components/screenshot/operations/Rectangle/index.tsx +245 -0
- package/src/components/screenshot/operations/Redo/index.tsx +22 -0
- package/src/components/screenshot/operations/Save/index.tsx +37 -0
- package/src/components/screenshot/operations/Scan/index.tsx +46 -0
- package/src/components/screenshot/operations/Search/index.tsx +39 -0
- package/src/components/screenshot/operations/Text/index.tsx +307 -0
- package/src/components/screenshot/operations/Undo/index.tsx +22 -0
- package/src/components/screenshot/operations/index.ts +34 -0
- package/src/components/screenshot/operations/utils.ts +34 -0
- package/src/components/screenshot/screenshots.scss +13 -0
- package/src/components/screenshot/types.ts +53 -0
- package/src/components/screenshot/useGetLoadedImage.ts +29 -0
- package/src/components/screenshot/var.scss +107 -0
- package/src/components/screenshot/zh_CN.ts +37 -0
- package/src/emoji/100.gif +0 -0
- package/src/emoji/101.gif +0 -0
- package/src/emoji/102.gif +0 -0
- package/src/emoji/103.gif +0 -0
- package/src/emoji/104.gif +0 -0
- package/src/emoji/105.gif +0 -0
- package/src/emoji/106.gif +0 -0
- package/src/emoji/107.gif +0 -0
- package/src/emoji/108.gif +0 -0
- package/src/emoji/109.gif +0 -0
- package/src/emoji/110.gif +0 -0
- package/src/emoji/111.gif +0 -0
- package/src/emoji/112.gif +0 -0
- package/src/emoji/113.gif +0 -0
- package/src/emoji/114.gif +0 -0
- package/src/emoji/115.gif +0 -0
- package/src/emoji/116.gif +0 -0
- package/src/emoji/117.gif +0 -0
- package/src/emoji/118.gif +0 -0
- package/src/emoji/119.gif +0 -0
- package/src/emoji/120.gif +0 -0
- package/src/emoji/121.gif +0 -0
- package/src/emoji/122.gif +0 -0
- package/src/emoji/123.gif +0 -0
- package/src/emoji/124.gif +0 -0
- package/src/emoji/125.gif +0 -0
- package/src/emoji/126.gif +0 -0
- package/src/emoji/127.gif +0 -0
- package/src/emoji/128.gif +0 -0
- package/src/emoji/129.gif +0 -0
- package/src/emoji/130.gif +0 -0
- package/src/emoji/131.gif +0 -0
- package/src/emoji/132.gif +0 -0
- package/src/emoji/133.gif +0 -0
- package/src/emoji/134.gif +0 -0
- package/src/emoji/135.gif +0 -0
- package/src/emoji/136.gif +0 -0
- package/src/emoji/137.gif +0 -0
- package/src/emoji/138.gif +0 -0
- package/src/emoji/139.gif +0 -0
- package/src/emoji/140.gif +0 -0
- package/src/emoji/141.gif +0 -0
- package/src/emoji/142.gif +0 -0
- package/src/emoji/143.gif +0 -0
- package/src/emoji/144.gif +0 -0
- package/src/emoji/145.gif +0 -0
- package/src/emoji/146.gif +0 -0
- package/src/emoji/147.gif +0 -0
- package/src/emoji/148.gif +0 -0
- package/src/emoji/149.gif +0 -0
- package/src/emoji/150.gif +0 -0
- package/src/emoji/151.gif +0 -0
- package/src/emoji/152.gif +0 -0
- package/src/emoji/153.gif +0 -0
- package/src/emoji/154.gif +0 -0
- package/src/emoji/155.gif +0 -0
- package/src/emoji/156.gif +0 -0
- package/src/emoji/157.gif +0 -0
- package/src/emoji/158.gif +0 -0
- package/src/emoji/159.gif +0 -0
- package/src/emoji/160.gif +0 -0
- package/src/emoji/161.gif +0 -0
- package/src/emoji/162.gif +0 -0
- package/src/emoji/163.gif +0 -0
- package/src/emoji/164.gif +0 -0
- package/src/emoji/165.gif +0 -0
- package/src/emoji/166.gif +0 -0
- package/src/emoji/167.gif +0 -0
- package/src/emoji/168.gif +0 -0
- package/src/emoji/169.gif +0 -0
- package/src/emoji/170.gif +0 -0
- package/src/emoji/171.gif +0 -0
- package/src/emoji/172.gif +0 -0
- package/src/emoji/173.gif +0 -0
- package/src/emoji/174.gif +0 -0
- package/src/emoji/175.gif +0 -0
- package/src/emoji/176.gif +0 -0
- package/src/emoji/177.gif +0 -0
- package/src/emoji/178.gif +0 -0
- package/src/emoji/179.gif +0 -0
- package/src/emoji/180.gif +0 -0
- package/src/emoji/181.gif +0 -0
- package/src/emoji/182.gif +0 -0
- package/src/emoji/183.gif +0 -0
- package/src/emoji/184.gif +0 -0
- package/src/emoji/185.gif +0 -0
- package/src/emoji/186.gif +0 -0
- package/src/emoji/187.gif +0 -0
- package/src/emoji/188.gif +0 -0
- package/src/emoji/189.gif +0 -0
- package/src/emoji/190.gif +0 -0
- package/src/emoji/191.gif +0 -0
- package/src/emoji/192.gif +0 -0
- package/src/emoji/193.gif +0 -0
- package/src/emoji/194.gif +0 -0
- package/src/emoji/195.gif +0 -0
- package/src/emoji/196.gif +0 -0
- package/src/emoji/197.gif +0 -0
- package/src/emoji/198.gif +0 -0
- package/src/emoji/199.gif +0 -0
- package/src/emoji/200.png +0 -0
- package/src/emoji/201.png +0 -0
- package/src/emoji/202.png +0 -0
- package/src/emoji/203.png +0 -0
- package/src/emoji/204.png +0 -0
- package/src/emoji/205.png +0 -0
- package/src/emoji/206.png +0 -0
- package/src/emoji/207.png +0 -0
- package/src/emoji/208.png +0 -0
- package/src/emoji/209.png +0 -0
- package/src/emoji/210.png +0 -0
- package/src/emoji/211.png +0 -0
- package/src/emoji/212.png +0 -0
- package/src/emoji/213.png +0 -0
- package/src/emoji/214.png +0 -0
- package/src/emoji/215.png +0 -0
- package/src/emoji/216.png +0 -0
- package/src/emoji/217.png +0 -0
- package/src/emoji/218.png +0 -0
- package/src/emoji/219.png +0 -0
- package/src/emoji2/101--Streamline-The-Team.png +0 -0
- package/src/emoji2/128--Streamline-The-Team.png +0 -0
- package/src/emoji2/134--Streamline-The-Team.png +0 -0
- package/src/emoji2/173--Streamline-The-Team.png +0 -0
- package/src/emoji2/Airplane--Streamline-Emoji.svg +24 -0
- package/src/emoji2/Alien--Streamline-Emoji.svg +21 -0
- package/src/emoji2/Amazed-Face--Streamline-Emoji.svg +16 -0
- package/src/emoji2/Amusing-Face--Streamline-Emoji.svg +20 -0
- package/src/emoji2/Anguished-Face--Streamline-Emoji.svg +19 -0
- package/src/emoji2/Anxious-Face--Streamline-Emoji.svg +17 -0
- package/src/emoji2/Astonished-Face--Streamline-Emoji.svg +20 -0
- package/src/emoji2/Backhand-Index-Pointing-Down-1--Streamline-Emoji.svg +12 -0
- package/src/emoji2/Backhand-Index-Pointing-Left-1--Streamline-Emoji.svg +13 -0
- package/src/emoji2/Backhand-Index-Pointing-Right-1--Streamline-Emoji.svg +13 -0
- package/src/emoji2/Backhand-Index-Pointing-Up-1--Streamline-Emoji.svg +14 -0
- package/src/emoji2/Bar-Chart--Streamline-Emoji.svg +22 -0
- package/src/emoji2/Beaming-Face-With-Smiling-Eyes--Streamline-Emoji.svg +20 -0
- package/src/emoji2/Boy-1--Streamline-Emoji.svg +17 -0
- package/src/emoji2/Boy-2--Streamline-Emoji.svg +17 -0
- package/src/emoji2/Boy-3--Streamline-Emoji.svg +17 -0
- package/src/emoji2/Broken-Heart--Streamline-Emoji.svg +12 -0
- package/src/emoji2/Clapping-Hands-1--Streamline-Emoji.svg +23 -0
- package/src/emoji2/Clinking-Glasses-2--Streamline-Emoji.svg +43 -0
- package/src/emoji2/Confounded-Face--Streamline-Emoji.svg +17 -0
- package/src/emoji2/Confused-Face--Streamline-Emoji.svg +15 -0
- package/src/emoji2/Construction-Worker--Streamline-Emoji.svg +21 -0
- package/src/emoji2/Couple-With-Heart-Woman-Man-1--Streamline-Emoji.svg +40 -0
- package/src/emoji2/Couple-With-Heart-Woman-Man-2--Streamline-Emoji.svg +40 -0
- package/src/emoji2/Cowboy-Hat-Face--Streamline-Emoji.svg +22 -0
- package/src/emoji2/Crazy-Face--Streamline-Emoji.svg +25 -0
- package/src/emoji2/Crossed-Fingers-1--Streamline-Emoji.svg +25 -0
- package/src/emoji2/Crown--Streamline-Emoji.svg +35 -0
- package/src/emoji2/Crying-Face--Streamline-Emoji.svg +26 -0
- package/src/emoji2/Delivery-Truck--Streamline-Emoji.svg +31 -0
- package/src/emoji2/Determined-Face--Streamline-Emoji.svg +25 -0
- package/src/emoji2/Disappointed-Face--Streamline-Emoji.svg +15 -0
- package/src/emoji2/Dizzy-Face--Streamline-Emoji.svg +20 -0
- package/src/emoji2/Downcast-Face-With-Sweat--Streamline-Emoji.svg +18 -0
- package/src/emoji2/Drooling-Face-1--Streamline-Emoji.svg +19 -0
- package/src/emoji2/Drooling-Face-2--Streamline-Emoji.svg +18 -0
- package/src/emoji2/Ear--Streamline-Emoji.svg +14 -0
- package/src/emoji2/Exclamation-Mark--Streamline-Emoji.svg +12 -0
- package/src/emoji2/Exploding-Head--Streamline-Emoji.svg +24 -0
- package/src/emoji2/Expressionless-Face--Streamline-Emoji.svg +15 -0
- package/src/emoji2/Face-Blowing-A-Kiss--Streamline-Emoji.svg +18 -0
- package/src/emoji2/Face-Savoring-Food--Streamline-Emoji.svg +18 -0
- package/src/emoji2/Face-Screaming-In-Fear--Streamline-Emoji.svg +18 -0
- package/src/emoji2/Face-Vomiting--Streamline-Emoji.svg +28 -0
- package/src/emoji2/Face-With-Head-Bandage--Streamline-Emoji.svg +25 -0
- package/src/emoji2/Face-With-Medical-Mask--Streamline-Emoji.svg +23 -0
- package/src/emoji2/Face-With-Monocle--Streamline-Emoji.svg +26 -0
- package/src/emoji2/Face-With-Raised-Eyebrow--Streamline-Emoji.svg +17 -0
- package/src/emoji2/Face-With-Rolling-Eyes--Streamline-Emoji.svg +17 -0
- package/src/emoji2/Face-With-Steam-From-Nose--Streamline-Emoji.svg +19 -0
- package/src/emoji2/Face-With-Symbols-On-Mouth--Streamline-Emoji.svg +22 -0
- package/src/emoji2/Face-With-Tears-Of-Joy--Streamline-Emoji.svg +34 -0
- package/src/emoji2/Face-With-Thermometer--Streamline-Emoji.svg +31 -0
- package/src/emoji2/Face-With-Tongue--Streamline-Emoji.svg +18 -0
- package/src/emoji2/Face-Without-Mouth--Streamline-Emoji.svg +14 -0
- package/src/emoji2/Fearful-Face--Streamline-Emoji.svg +18 -0
- package/src/emoji2/Flexed-Biceps-1--Streamline-Emoji.svg +13 -0
- package/src/emoji2/Flushed-Face--Streamline-Emoji.svg +19 -0
- package/src/emoji2/Folded-Hands-1--Streamline-Emoji.svg +29 -0
- package/src/emoji2/Frowning-Face--Streamline-Emoji.svg +15 -0
- package/src/emoji2/Fuel-Pump--Streamline-Emoji.svg +30 -0
- package/src/emoji2/Girl-1--Streamline-Emoji.svg +23 -0
- package/src/emoji2/Glasses-1--Streamline-Emoji.svg +27 -0
- package/src/emoji2/Grimacing-Face--Streamline-Emoji.svg +19 -0
- package/src/emoji2/Grinning-Cat-Face--Streamline-Emoji.svg +32 -0
- package/src/emoji2/Grinning-Face--Streamline-Emoji.svg +16 -0
- package/src/emoji2/Grinning-Face-With-Sweat--Streamline-Emoji.svg +19 -0
- package/src/emoji2/Grinning-Squinting-Face--Streamline-Emoji.svg +16 -0
- package/src/emoji2/Hand-With-Fingers-Splayed-1--Streamline-Emoji.svg +14 -0
- package/src/emoji2/Heart-Suit--Streamline-Emoji.svg +9 -0
- package/src/emoji2/Hushed-Face-1--Streamline-Emoji.svg +17 -0
- package/src/emoji2/Hushed-Face-2--Streamline-Emoji.svg +15 -0
- package/src/emoji2/Index-Pointing-Up-1--Streamline-Emoji.svg +17 -0
- package/src/emoji2/Kissing-Face-With-Closed-Eyes--Streamline-Emoji.svg +18 -0
- package/src/emoji2/Loudly-Crying-Face--Streamline-Emoji.svg +16 -0
- package/src/emoji2/Lying-Face--Streamline-Emoji.svg +15 -0
- package/src/emoji2/Man-1--Streamline-Emoji.svg +17 -0
- package/src/emoji2/Man-Facepalming-1--Streamline-Emoji.svg +21 -0
- package/src/emoji2/Man-Gesturing-No-1--Streamline-Emoji.svg +34 -0
- package/src/emoji2/Man-Gesturing-Ok-1--Streamline-Emoji.svg +25 -0
- package/src/emoji2/Man-Health-Worker-1--Streamline-Emoji.svg +41 -0
- package/src/emoji2/Man-Raising-Hand-1--Streamline-Emoji.svg +26 -0
- package/src/emoji2/Man-Shrugging-1--Streamline-Emoji.svg +31 -0
- package/src/emoji2/Money-Mouth-Face-2--Streamline-Emoji.svg +30 -0
- package/src/emoji2/Mouth--Streamline-Emoji.svg +12 -0
- package/src/emoji2/Nauseated-Face-2--Streamline-Emoji.svg +19 -0
- package/src/emoji2/Neutral-Face--Streamline-Emoji.svg +15 -0
- package/src/emoji2/Ok-Hand-1--Streamline-Emoji.svg +14 -0
- package/src/emoji2/Old-Man-1--Streamline-Emoji.svg +27 -0
- package/src/emoji2/Old-Woman-1--Streamline-Emoji.svg +23 -0
- package/src/emoji2/Oncoming-Fist-1--Streamline-Emoji.svg +15 -0
- package/src/emoji2/Person-Wearing-Turban-2--Streamline-Emoji.svg +20 -0
- package/src/emoji2/Pile-Of-Poo--Streamline-Emoji.svg +15 -0
- package/src/emoji2/Police-Car-Light--Streamline-Emoji.svg +26 -0
- package/src/emoji2/Rocket--Streamline-Emoji.svg +32 -0
- package/src/emoji2/Sailboat--Streamline-Emoji.svg +18 -0
- package/src/emoji2/Shaved-Ice--Streamline-Emoji.svg +21 -0
- package/src/emoji2/Shortcake-2--Streamline-Emoji.svg +18 -0
- package/src/emoji2/Shushing-Face--Streamline-Emoji.svg +21 -0
- package/src/emoji2/Sign-Of-The-Horns-1--Streamline-Emoji.svg +19 -0
- package/src/emoji2/Sleeping-Face--Streamline-Emoji.svg +21 -0
- package/src/emoji2/Slightly-Smiling-Face--Streamline-Emoji.svg +15 -0
- package/src/emoji2/Smiling-Face-With-Halo--Streamline-Emoji.svg +20 -0
- package/src/emoji2/Smiling-Face-With-Heart-Eyes--Streamline-Emoji.svg +21 -0
- package/src/emoji2/Smirking-Face--Streamline-Emoji.svg +17 -0
- package/src/emoji2/Sun-With-Face--Streamline-Emoji.svg +24 -0
- package/src/emoji2/Thumbs-Down-1--Streamline-Emoji.svg +20 -0
- package/src/emoji2/Thumbs-Up-1--Streamline-Emoji.svg +19 -0
- package/src/emoji2/Winking-Face--Streamline-Emoji.svg +18 -0
- package/src/emoji2/Woman-Gesturing-No-1--Streamline-Emoji.svg +34 -0
- package/src/emoji2/Woman-Gesturing-Ok-2--Streamline-Emoji.svg +25 -0
- package/src/emoji2/Woman-Raising-Hand-1--Streamline-Emoji.svg +26 -0
- package/src/emoji2/Womans-Sandal--Streamline-Emoji.svg +13 -0
- package/src/emoji2/Worried-Face--Streamline-Emoji.svg +17 -0
- package/src/emoji2/Writing-Hand-1--Streamline-Emoji.svg +17 -0
- package/src/emoji2/Zipper-Mouth-Face--Streamline-Emoji.svg +21 -0
- package/src/index.js +19 -0
- package/src/services/burn-after-read-service.js +313 -0
- package/src/services/burn-after-read-service.test.js +325 -0
- package/src/services/dify-api.js +338 -0
- package/src/services/dify-api.test.js +376 -0
- package/src/services/scheduled-send-service.js +311 -0
- package/src/services/scheduled-send-service.test.js +317 -0
- package/src/styles/index.css +2368 -0
- package/src/utils/emoji.js +125 -0
- package/src/utils/emojiData.js +267 -0
- package/src/utils/eventEmitter.js +114 -0
- package/src/utils/state.js +224 -0
- package/src/utils/state.test.js +198 -0
- package/src/utils/storage.js +122 -0
- package/src/utils/storage.test.js +162 -0
- package/src/utils/validation.js +249 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Management Utilities
|
|
3
|
+
* Provides composable state management for chat features
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ref, computed, watch } from 'vue';
|
|
7
|
+
import { saveToStorage, loadFromStorage } from './storage';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create a persistent reactive state
|
|
11
|
+
* @param {string} key - Storage key
|
|
12
|
+
* @param {any} initialValue - Initial value
|
|
13
|
+
* @param {Object} options - Configuration options
|
|
14
|
+
* @returns {Object} - Reactive state with persistence
|
|
15
|
+
*/
|
|
16
|
+
export const usePersistentState = (key, initialValue, options = {}) => {
|
|
17
|
+
const {
|
|
18
|
+
persist = true,
|
|
19
|
+
debounce = 300,
|
|
20
|
+
transform = null,
|
|
21
|
+
validate = null
|
|
22
|
+
} = options;
|
|
23
|
+
|
|
24
|
+
// Load from storage or use initial value
|
|
25
|
+
const storedValue = persist ? loadFromStorage(key, initialValue) : initialValue;
|
|
26
|
+
const state = ref(storedValue);
|
|
27
|
+
|
|
28
|
+
// Validation
|
|
29
|
+
if (validate && !validate(state.value)) {
|
|
30
|
+
state.value = initialValue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Auto-persist with debounce
|
|
34
|
+
if (persist) {
|
|
35
|
+
let debounceTimer;
|
|
36
|
+
watch(
|
|
37
|
+
state,
|
|
38
|
+
(newValue) => {
|
|
39
|
+
clearTimeout(debounceTimer);
|
|
40
|
+
debounceTimer = setTimeout(() => {
|
|
41
|
+
const valueToSave = transform ? transform(newValue) : newValue;
|
|
42
|
+
saveToStorage(key, valueToSave);
|
|
43
|
+
}, debounce);
|
|
44
|
+
},
|
|
45
|
+
{ deep: true }
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return state;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create a computed state based on multiple sources
|
|
54
|
+
* @param {Function} computeFn - Computation function
|
|
55
|
+
* @returns {Object} - Computed state
|
|
56
|
+
*/
|
|
57
|
+
export const useComputedState = (computeFn) => {
|
|
58
|
+
return computed(computeFn);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create a state with history tracking
|
|
63
|
+
* @param {any} initialValue - Initial value
|
|
64
|
+
* @param {number} maxHistory - Maximum history size
|
|
65
|
+
* @returns {Object} - State with history methods
|
|
66
|
+
*/
|
|
67
|
+
export const useStateWithHistory = (initialValue, maxHistory = 50) => {
|
|
68
|
+
const state = ref(initialValue);
|
|
69
|
+
const history = ref([initialValue]);
|
|
70
|
+
const historyIndex = ref(0);
|
|
71
|
+
|
|
72
|
+
const push = (value) => {
|
|
73
|
+
// Remove future history if we're not at the end
|
|
74
|
+
if (historyIndex.value < history.value.length - 1) {
|
|
75
|
+
history.value = history.value.slice(0, historyIndex.value + 1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Add new state
|
|
79
|
+
history.value.push(value);
|
|
80
|
+
if (history.value.length > maxHistory) {
|
|
81
|
+
history.value.shift();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
historyIndex.value = history.value.length - 1;
|
|
85
|
+
state.value = value;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const undo = () => {
|
|
89
|
+
if (historyIndex.value > 0) {
|
|
90
|
+
historyIndex.value--;
|
|
91
|
+
state.value = history.value[historyIndex.value];
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const redo = () => {
|
|
96
|
+
if (historyIndex.value < history.value.length - 1) {
|
|
97
|
+
historyIndex.value++;
|
|
98
|
+
state.value = history.value[historyIndex.value];
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const canUndo = computed(() => historyIndex.value > 0);
|
|
103
|
+
const canRedo = computed(() => historyIndex.value < history.value.length - 1);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
state,
|
|
107
|
+
push,
|
|
108
|
+
undo,
|
|
109
|
+
redo,
|
|
110
|
+
canUndo,
|
|
111
|
+
canRedo,
|
|
112
|
+
history: computed(() => history.value)
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Create a state with async loading
|
|
118
|
+
* @param {Function} asyncFn - Async function to load data
|
|
119
|
+
* @param {any} initialValue - Initial value
|
|
120
|
+
* @returns {Object} - State with loading status
|
|
121
|
+
*/
|
|
122
|
+
export const useAsyncState = (asyncFn, initialValue = null) => {
|
|
123
|
+
const state = ref(initialValue);
|
|
124
|
+
const loading = ref(false);
|
|
125
|
+
const error = ref(null);
|
|
126
|
+
|
|
127
|
+
const load = async (...args) => {
|
|
128
|
+
loading.value = true;
|
|
129
|
+
error.value = null;
|
|
130
|
+
try {
|
|
131
|
+
state.value = await asyncFn(...args);
|
|
132
|
+
} catch (e) {
|
|
133
|
+
error.value = e;
|
|
134
|
+
console.error('Async state error:', e);
|
|
135
|
+
} finally {
|
|
136
|
+
loading.value = false;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
state,
|
|
142
|
+
loading,
|
|
143
|
+
error,
|
|
144
|
+
load
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Create a state with validation
|
|
150
|
+
* @param {any} initialValue - Initial value
|
|
151
|
+
* @param {Function} validator - Validation function
|
|
152
|
+
* @returns {Object} - State with validation
|
|
153
|
+
*/
|
|
154
|
+
export const useValidatedState = (initialValue, validator) => {
|
|
155
|
+
const state = ref(initialValue);
|
|
156
|
+
const errors = ref([]);
|
|
157
|
+
|
|
158
|
+
const validate = () => {
|
|
159
|
+
errors.value = validator(state.value) || [];
|
|
160
|
+
return errors.value.length === 0;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const isValid = computed(() => errors.value.length === 0);
|
|
164
|
+
|
|
165
|
+
watch(state, () => {
|
|
166
|
+
validate();
|
|
167
|
+
}, { deep: true });
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
state,
|
|
171
|
+
errors,
|
|
172
|
+
validate,
|
|
173
|
+
isValid
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Create a state with reset capability
|
|
179
|
+
* @param {any} initialValue - Initial value
|
|
180
|
+
* @returns {Object} - State with reset method
|
|
181
|
+
*/
|
|
182
|
+
export const useResettableState = (initialValue) => {
|
|
183
|
+
const state = ref(initialValue);
|
|
184
|
+
|
|
185
|
+
const reset = () => {
|
|
186
|
+
state.value = initialValue;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const isDirty = computed(() => {
|
|
190
|
+
return JSON.stringify(state.value) !== JSON.stringify(initialValue);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
state,
|
|
195
|
+
reset,
|
|
196
|
+
isDirty
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Create a state with debounced updates
|
|
202
|
+
* @param {any} initialValue - Initial value
|
|
203
|
+
* @param {number} delay - Debounce delay in ms
|
|
204
|
+
* @returns {Object} - State with debounced setter
|
|
205
|
+
*/
|
|
206
|
+
export const useDebouncedState = (initialValue, delay = 300) => {
|
|
207
|
+
const state = ref(initialValue);
|
|
208
|
+
const debouncedState = ref(initialValue);
|
|
209
|
+
let debounceTimer;
|
|
210
|
+
|
|
211
|
+
const setState = (value) => {
|
|
212
|
+
state.value = value;
|
|
213
|
+
clearTimeout(debounceTimer);
|
|
214
|
+
debounceTimer = setTimeout(() => {
|
|
215
|
+
debouncedState.value = value;
|
|
216
|
+
}, delay);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
state,
|
|
221
|
+
debouncedState,
|
|
222
|
+
setState
|
|
223
|
+
};
|
|
224
|
+
};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { usePersistentState, useStateWithHistory, useResettableState, useDebouncedState } from './state';
|
|
3
|
+
|
|
4
|
+
describe('State Management Utilities', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
localStorage.clear();
|
|
7
|
+
vi.useFakeTimers();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.useRealTimers();
|
|
12
|
+
localStorage.clear();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('usePersistentState', () => {
|
|
16
|
+
it('should create a persistent reactive state', () => {
|
|
17
|
+
const state = usePersistentState('test-state', 'initial');
|
|
18
|
+
|
|
19
|
+
expect(state.value).toBe('initial');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should persist state to localStorage', async () => {
|
|
23
|
+
const state = usePersistentState('test-state', 'initial');
|
|
24
|
+
state.value = 'updated';
|
|
25
|
+
|
|
26
|
+
vi.advanceTimersByTime(300);
|
|
27
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
28
|
+
|
|
29
|
+
const stored = localStorage.getItem('cd-aichat-test-state');
|
|
30
|
+
expect(stored).toContain('updated');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should load persisted state on creation', () => {
|
|
34
|
+
localStorage.setItem('cd-aichat-test-state', JSON.stringify('persisted'));
|
|
35
|
+
const state = usePersistentState('test-state', 'initial');
|
|
36
|
+
|
|
37
|
+
expect(state.value).toBe('persisted');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should validate state on creation', () => {
|
|
41
|
+
const validator = (value) => value > 0;
|
|
42
|
+
const state = usePersistentState('test-state', 5, { validate: validator });
|
|
43
|
+
|
|
44
|
+
expect(state.value).toBe(5);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should reset to initial value if validation fails', () => {
|
|
48
|
+
localStorage.setItem('cd-aichat-test-state', JSON.stringify(-5));
|
|
49
|
+
const validator = (value) => value > 0;
|
|
50
|
+
const state = usePersistentState('test-state', 5, { validate: validator });
|
|
51
|
+
|
|
52
|
+
expect(state.value).toBe(5);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should not persist when persist is false', async () => {
|
|
56
|
+
const state = usePersistentState('test-state', 'initial', { persist: false });
|
|
57
|
+
state.value = 'updated';
|
|
58
|
+
|
|
59
|
+
vi.advanceTimersByTime(300);
|
|
60
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
61
|
+
|
|
62
|
+
const stored = localStorage.getItem('cd-aichat-test-state');
|
|
63
|
+
expect(stored).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('useStateWithHistory', () => {
|
|
68
|
+
it('should track state history', () => {
|
|
69
|
+
const { state, push, history } = useStateWithHistory('initial');
|
|
70
|
+
|
|
71
|
+
push('second');
|
|
72
|
+
push('third');
|
|
73
|
+
|
|
74
|
+
expect(history.value).toEqual(['initial', 'second', 'third']);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should support undo', () => {
|
|
78
|
+
const { state, push, undo } = useStateWithHistory('initial');
|
|
79
|
+
|
|
80
|
+
push('second');
|
|
81
|
+
push('third');
|
|
82
|
+
undo();
|
|
83
|
+
|
|
84
|
+
expect(state.value).toBe('second');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should support redo', () => {
|
|
88
|
+
const { state, push, undo, redo } = useStateWithHistory('initial');
|
|
89
|
+
|
|
90
|
+
push('second');
|
|
91
|
+
push('third');
|
|
92
|
+
undo();
|
|
93
|
+
redo();
|
|
94
|
+
|
|
95
|
+
expect(state.value).toBe('third');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should track undo/redo availability', () => {
|
|
99
|
+
const { push, undo, redo, canUndo, canRedo } = useStateWithHistory('initial');
|
|
100
|
+
|
|
101
|
+
expect(canUndo.value).toBe(false);
|
|
102
|
+
expect(canRedo.value).toBe(false);
|
|
103
|
+
|
|
104
|
+
push('second');
|
|
105
|
+
expect(canUndo.value).toBe(true);
|
|
106
|
+
|
|
107
|
+
undo();
|
|
108
|
+
expect(canRedo.value).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should limit history size', () => {
|
|
112
|
+
const { push, history } = useStateWithHistory('initial', 3);
|
|
113
|
+
|
|
114
|
+
push('second');
|
|
115
|
+
push('third');
|
|
116
|
+
push('fourth');
|
|
117
|
+
push('fifth');
|
|
118
|
+
|
|
119
|
+
expect(history.value.length).toBe(3);
|
|
120
|
+
expect(history.value).toEqual(['third', 'fourth', 'fifth']);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should clear future history on new push after undo', () => {
|
|
124
|
+
const { push, undo, history } = useStateWithHistory('initial');
|
|
125
|
+
|
|
126
|
+
push('second');
|
|
127
|
+
push('third');
|
|
128
|
+
undo();
|
|
129
|
+
push('new');
|
|
130
|
+
|
|
131
|
+
expect(history.value).toEqual(['initial', 'second', 'new']);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('useResettableState', () => {
|
|
136
|
+
it('should create a resettable state', () => {
|
|
137
|
+
const { state, reset } = useResettableState('initial');
|
|
138
|
+
|
|
139
|
+
state.value = 'modified';
|
|
140
|
+
reset();
|
|
141
|
+
|
|
142
|
+
expect(state.value).toBe('initial');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should track dirty state', () => {
|
|
146
|
+
const { state, isDirty } = useResettableState('initial');
|
|
147
|
+
|
|
148
|
+
expect(isDirty.value).toBe(false);
|
|
149
|
+
|
|
150
|
+
state.value = 'modified';
|
|
151
|
+
expect(isDirty.value).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should handle complex objects', () => {
|
|
155
|
+
const initial = { count: 0, name: 'test' };
|
|
156
|
+
const { state, reset, isDirty } = useResettableState(initial);
|
|
157
|
+
|
|
158
|
+
state.value.count = 5;
|
|
159
|
+
expect(isDirty.value).toBe(true);
|
|
160
|
+
|
|
161
|
+
reset();
|
|
162
|
+
expect(state.value).toEqual(initial);
|
|
163
|
+
expect(isDirty.value).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('useDebouncedState', () => {
|
|
168
|
+
it('should debounce state updates', async () => {
|
|
169
|
+
const { state, debouncedState, setState } = useDebouncedState('initial', 300);
|
|
170
|
+
|
|
171
|
+
setState('updated');
|
|
172
|
+
expect(state.value).toBe('updated');
|
|
173
|
+
expect(debouncedState.value).toBe('initial');
|
|
174
|
+
|
|
175
|
+
vi.advanceTimersByTime(300);
|
|
176
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
177
|
+
|
|
178
|
+
expect(debouncedState.value).toBe('updated');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should cancel previous debounce on new update', async () => {
|
|
182
|
+
const { state, debouncedState, setState } = useDebouncedState('initial', 300);
|
|
183
|
+
|
|
184
|
+
setState('first');
|
|
185
|
+
vi.advanceTimersByTime(100);
|
|
186
|
+
|
|
187
|
+
setState('second');
|
|
188
|
+
vi.advanceTimersByTime(100);
|
|
189
|
+
|
|
190
|
+
expect(debouncedState.value).toBe('initial');
|
|
191
|
+
|
|
192
|
+
vi.advanceTimersByTime(200);
|
|
193
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
194
|
+
|
|
195
|
+
expect(debouncedState.value).toBe('second');
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage Utilities
|
|
3
|
+
* Provides localStorage persistence with error handling and size management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const STORAGE_PREFIX = 'cd-aichat-';
|
|
7
|
+
const MAX_STORAGE_SIZE = 5 * 1024 * 1024; // 5MB
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Save data to localStorage with error handling
|
|
11
|
+
* @param {string} key - Storage key
|
|
12
|
+
* @param {any} data - Data to store
|
|
13
|
+
* @returns {boolean} - Success status
|
|
14
|
+
*/
|
|
15
|
+
export const saveToStorage = (key, data) => {
|
|
16
|
+
try {
|
|
17
|
+
const prefixedKey = STORAGE_PREFIX + key;
|
|
18
|
+
const serialized = JSON.stringify(data);
|
|
19
|
+
|
|
20
|
+
// Check size
|
|
21
|
+
if (serialized.length > MAX_STORAGE_SIZE) {
|
|
22
|
+
console.warn(`Storage data for key "${key}" exceeds size limit`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
localStorage.setItem(prefixedKey, serialized);
|
|
26
|
+
return true;
|
|
27
|
+
} catch (e) {
|
|
28
|
+
if (e.name === 'QuotaExceededError') {
|
|
29
|
+
console.error('localStorage quota exceeded');
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
console.error('Failed to save to storage:', e);
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Load data from localStorage with error handling
|
|
39
|
+
* @param {string} key - Storage key
|
|
40
|
+
* @param {any} defaultValue - Default value if not found
|
|
41
|
+
* @returns {any} - Stored data or default value
|
|
42
|
+
*/
|
|
43
|
+
export const loadFromStorage = (key, defaultValue = null) => {
|
|
44
|
+
try {
|
|
45
|
+
const prefixedKey = STORAGE_PREFIX + key;
|
|
46
|
+
const item = localStorage.getItem(prefixedKey);
|
|
47
|
+
return item ? JSON.parse(item) : defaultValue;
|
|
48
|
+
} catch (e) {
|
|
49
|
+
console.error('Failed to load from storage:', e);
|
|
50
|
+
return defaultValue;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Remove data from localStorage
|
|
56
|
+
* @param {string} key - Storage key
|
|
57
|
+
* @returns {boolean} - Success status
|
|
58
|
+
*/
|
|
59
|
+
export const removeFromStorage = (key) => {
|
|
60
|
+
try {
|
|
61
|
+
const prefixedKey = STORAGE_PREFIX + key;
|
|
62
|
+
localStorage.removeItem(prefixedKey);
|
|
63
|
+
return true;
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.error('Failed to remove from storage:', e);
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Clear all prefixed storage items
|
|
72
|
+
* @returns {boolean} - Success status
|
|
73
|
+
*/
|
|
74
|
+
export const clearStorage = () => {
|
|
75
|
+
try {
|
|
76
|
+
const keys = Object.keys(localStorage);
|
|
77
|
+
keys.forEach(key => {
|
|
78
|
+
if (key.startsWith(STORAGE_PREFIX)) {
|
|
79
|
+
localStorage.removeItem(key);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
return true;
|
|
83
|
+
} catch (e) {
|
|
84
|
+
console.error('Failed to clear storage:', e);
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get storage size in bytes
|
|
91
|
+
* @returns {number} - Total size of prefixed items
|
|
92
|
+
*/
|
|
93
|
+
export const getStorageSize = () => {
|
|
94
|
+
try {
|
|
95
|
+
let size = 0;
|
|
96
|
+
const keys = Object.keys(localStorage);
|
|
97
|
+
keys.forEach(key => {
|
|
98
|
+
if (key.startsWith(STORAGE_PREFIX)) {
|
|
99
|
+
size += localStorage.getItem(key).length;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
return size;
|
|
103
|
+
} catch (e) {
|
|
104
|
+
console.error('Failed to get storage size:', e);
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Check if storage is available
|
|
111
|
+
* @returns {boolean} - Storage availability
|
|
112
|
+
*/
|
|
113
|
+
export const isStorageAvailable = () => {
|
|
114
|
+
try {
|
|
115
|
+
const test = '__storage_test__';
|
|
116
|
+
localStorage.setItem(test, test);
|
|
117
|
+
localStorage.removeItem(test);
|
|
118
|
+
return true;
|
|
119
|
+
} catch (e) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
saveToStorage,
|
|
4
|
+
loadFromStorage,
|
|
5
|
+
removeFromStorage,
|
|
6
|
+
clearStorage,
|
|
7
|
+
getStorageSize,
|
|
8
|
+
isStorageAvailable
|
|
9
|
+
} from './storage';
|
|
10
|
+
|
|
11
|
+
describe('Storage Utilities', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
localStorage.clear();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
localStorage.clear();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('saveToStorage', () => {
|
|
21
|
+
it('should save data to localStorage', () => {
|
|
22
|
+
const key = 'test-key';
|
|
23
|
+
const data = { message: 'test' };
|
|
24
|
+
|
|
25
|
+
const result = saveToStorage(key, data);
|
|
26
|
+
|
|
27
|
+
expect(result).toBe(true);
|
|
28
|
+
expect(localStorage.getItem('cd-aichat-test-key')).toBeDefined();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should handle complex objects', () => {
|
|
32
|
+
const key = 'complex';
|
|
33
|
+
const data = {
|
|
34
|
+
nested: { value: 123 },
|
|
35
|
+
array: [1, 2, 3],
|
|
36
|
+
date: new Date().toISOString()
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
saveToStorage(key, data);
|
|
40
|
+
const loaded = loadFromStorage(key);
|
|
41
|
+
|
|
42
|
+
expect(loaded).toEqual(data);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should return false on quota exceeded', () => {
|
|
46
|
+
// Mock localStorage to throw QuotaExceededError
|
|
47
|
+
const originalSetItem = localStorage.setItem;
|
|
48
|
+
localStorage.setItem = vi.fn(() => {
|
|
49
|
+
const error = new Error('QuotaExceededError');
|
|
50
|
+
error.name = 'QuotaExceededError';
|
|
51
|
+
throw error;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const result = saveToStorage('test', { data: 'test' });
|
|
55
|
+
|
|
56
|
+
expect(result).toBe(false);
|
|
57
|
+
localStorage.setItem = originalSetItem;
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('loadFromStorage', () => {
|
|
62
|
+
it('should load data from localStorage', () => {
|
|
63
|
+
const key = 'test-key';
|
|
64
|
+
const data = { message: 'test' };
|
|
65
|
+
|
|
66
|
+
saveToStorage(key, data);
|
|
67
|
+
const loaded = loadFromStorage(key);
|
|
68
|
+
|
|
69
|
+
expect(loaded).toEqual(data);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should return default value if key not found', () => {
|
|
73
|
+
const defaultValue = { default: true };
|
|
74
|
+
const loaded = loadFromStorage('non-existent', defaultValue);
|
|
75
|
+
|
|
76
|
+
expect(loaded).toEqual(defaultValue);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should return null if no default provided', () => {
|
|
80
|
+
const loaded = loadFromStorage('non-existent');
|
|
81
|
+
|
|
82
|
+
expect(loaded).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should handle corrupted data gracefully', () => {
|
|
86
|
+
localStorage.setItem('cd-aichat-corrupted', 'invalid json {');
|
|
87
|
+
const defaultValue = { default: true };
|
|
88
|
+
const loaded = loadFromStorage('corrupted', defaultValue);
|
|
89
|
+
|
|
90
|
+
expect(loaded).toEqual(defaultValue);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('removeFromStorage', () => {
|
|
95
|
+
it('should remove data from localStorage', () => {
|
|
96
|
+
const key = 'test-key';
|
|
97
|
+
saveToStorage(key, { data: 'test' });
|
|
98
|
+
|
|
99
|
+
const result = removeFromStorage(key);
|
|
100
|
+
|
|
101
|
+
expect(result).toBe(true);
|
|
102
|
+
expect(loadFromStorage(key)).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should handle non-existent keys', () => {
|
|
106
|
+
const result = removeFromStorage('non-existent');
|
|
107
|
+
|
|
108
|
+
expect(result).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('clearStorage', () => {
|
|
113
|
+
it('should clear all prefixed storage items', () => {
|
|
114
|
+
saveToStorage('key1', { data: 1 });
|
|
115
|
+
saveToStorage('key2', { data: 2 });
|
|
116
|
+
localStorage.setItem('other-key', 'value');
|
|
117
|
+
|
|
118
|
+
clearStorage();
|
|
119
|
+
|
|
120
|
+
expect(loadFromStorage('key1')).toBeNull();
|
|
121
|
+
expect(loadFromStorage('key2')).toBeNull();
|
|
122
|
+
expect(localStorage.getItem('other-key')).toBe('value');
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('getStorageSize', () => {
|
|
127
|
+
it('should calculate storage size', () => {
|
|
128
|
+
saveToStorage('key1', { data: 'test' });
|
|
129
|
+
saveToStorage('key2', { data: 'test' });
|
|
130
|
+
|
|
131
|
+
const size = getStorageSize();
|
|
132
|
+
|
|
133
|
+
expect(size).toBeGreaterThan(0);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should return 0 for empty storage', () => {
|
|
137
|
+
const size = getStorageSize();
|
|
138
|
+
|
|
139
|
+
expect(size).toBe(0);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('isStorageAvailable', () => {
|
|
144
|
+
it('should return true when storage is available', () => {
|
|
145
|
+
const available = isStorageAvailable();
|
|
146
|
+
|
|
147
|
+
expect(available).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should return false when storage is not available', () => {
|
|
151
|
+
const originalSetItem = localStorage.setItem;
|
|
152
|
+
localStorage.setItem = vi.fn(() => {
|
|
153
|
+
throw new Error('Storage not available');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const available = isStorageAvailable();
|
|
157
|
+
|
|
158
|
+
expect(available).toBe(false);
|
|
159
|
+
localStorage.setItem = originalSetItem;
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|